Adds basic register logic

- Adds register endpoint in the API
- Adds the Gatekeeper to keep track of registered user and allow/reject access
- Adds registration errors
- Updates API unit tests
- Refactors some methods of the API to reduce code replication
This commit is contained in:
Sergi Delgado Segura
2020-03-25 12:14:12 +01:00
parent dd53ad68fb
commit 519caec29a
5 changed files with 113 additions and 26 deletions

View File

@@ -2,10 +2,11 @@ import os
import logging import logging
from flask import Flask, request, abort, jsonify from flask import Flask, request, abort, jsonify
import teos.errors as errors
from teos import HOST, PORT, LOG_PREFIX from teos import HOST, PORT, LOG_PREFIX
from common.logger import Logger from common.logger import Logger
from common.appointment import Appointment from common.appointment import Appointment
from common.constants import HTTP_OK, HTTP_BAD_REQUEST, HTTP_SERVICE_UNAVAILABLE, LOCATOR_LEN_HEX 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) 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: class API:
""" """
The :class:`API` is in charge of the interface between the user and the tower. It handles and server user requests. 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 <teos.watcher.Watcher>`): a ``Watcher`` instance to pass the requests to. watcher (:obj:`Watcher <teos.watcher.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.inspector = inspector
self.watcher = watcher 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): def add_appointment(self):
""" """
@@ -43,15 +101,10 @@ class API:
""" """
# Getting the real IP if the server is behind a reverse proxy # Getting the real IP if the server is behind a reverse proxy
remote_addr = request.environ.get("HTTP_X_REAL_IP") remote_addr = get_remote_addr()
if not remote_addr:
remote_addr = request.environ.get("REMOTE_ADDR")
logger.info("Received add_appointment request", from_addr="{}".format(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: if request.is_json:
# Check content type once if properly defined # Check content type once if properly defined
request_data = request.get_json() request_data = request.get_json()
@@ -59,9 +112,6 @@ class API:
request_data.get("appointment"), request_data.get("signature"), request_data.get("public_key") request_data.get("appointment"), request_data.get("signature"), request_data.get("public_key")
) )
error = None
response = None
if type(appointment) == Appointment: if type(appointment) == Appointment:
appointment_added, signature = self.watcher.add_appointment(appointment) appointment_added, signature = self.watcher.add_appointment(appointment)
@@ -72,29 +122,26 @@ class API:
else: else:
rcode = HTTP_SERVICE_UNAVAILABLE rcode = HTTP_SERVICE_UNAVAILABLE
error = "appointment rejected" error = "appointment rejected"
response = {"error": error}
elif type(appointment) == tuple: elif type(appointment) == tuple:
rcode = HTTP_BAD_REQUEST rcode = HTTP_BAD_REQUEST
error = "appointment rejected. Error {}: {}".format(appointment[0], appointment[1]) error = "appointment rejected. Error {}: {}".format(appointment[0], appointment[1])
response = {"error": error}
else: else:
# We should never end up here, since inspect only returns appointments or tuples. Just in case. # We should never end up here, since inspect only returns appointments or tuples. Just in case.
rcode = HTTP_BAD_REQUEST rcode = HTTP_BAD_REQUEST
error = "appointment rejected. Request does not match the standard" error = "appointment rejected. Request does not match the standard"
response = {"error": error}
else: else:
rcode = HTTP_BAD_REQUEST rcode = HTTP_BAD_REQUEST
error = "appointment rejected. Request is not json encoded" error = "appointment rejected. Request is not json encoded"
response = None response = {"error": error}
logger.info( logger.info("Sending response and disconnecting", from_addr="{}".format(remote_addr), response=response)
"Sending response and disconnecting", from_addr="{}".format(remote_addr), response=response, error=error return jsonify(response), rcode
)
if error is None:
return jsonify(response), rcode
else:
return jsonify({"error": error}), rcode
# FIXME: THE NEXT TWO API ENDPOINTS ARE FOR TESTING AND SHOULD BE REMOVED / PROPERLY MANAGED BEFORE PRODUCTION! # FIXME: THE NEXT TWO API ENDPOINTS ARE FOR TESTING AND SHOULD BE REMOVED / PROPERLY MANAGED BEFORE PRODUCTION!
# ToDo: #17-add-api-keys # ToDo: #17-add-api-keys
@@ -116,9 +163,7 @@ class API:
""" """
# Getting the real IP if the server is behind a reverse proxy # Getting the real IP if the server is behind a reverse proxy
remote_addr = request.environ.get("HTTP_X_REAL_IP") remote_addr = get_remote_addr()
if not remote_addr:
remote_addr = request.environ.get("REMOTE_ADDR")
locator = request.args.get("locator") locator = request.args.get("locator")
response = [] response = []
@@ -182,12 +227,14 @@ class API:
return response return response
# TODO: UNITTEST
def start(self): def start(self):
""" """
This function starts the Flask server used to run the API. Adds all the routes to the functions listed above. This function starts the Flask server used to run the API. Adds all the routes to the functions listed above.
""" """
routes = { routes = {
"/register": (self.register, ["POST"]),
"/add_appointment": (self.add_appointment, ["POST"]), "/add_appointment": (self.add_appointment, ["POST"]),
"/get_appointment": (self.get_appointment, ["GET"]), "/get_appointment": (self.get_appointment, ["GET"]),
"/get_all_appointments": (self.get_all_appointments, ["GET"]), "/get_all_appointments": (self.get_all_appointments, ["GET"]),

View File

@@ -1,4 +1,4 @@
# Appointment errors # Appointment errors [-1, -64]
APPOINTMENT_EMPTY_FIELD = -1 APPOINTMENT_EMPTY_FIELD = -1
APPOINTMENT_WRONG_FIELD_TYPE = -2 APPOINTMENT_WRONG_FIELD_TYPE = -2
APPOINTMENT_WRONG_FIELD_SIZE = -3 APPOINTMENT_WRONG_FIELD_SIZE = -3
@@ -8,6 +8,10 @@ APPOINTMENT_FIELD_TOO_BIG = -6
APPOINTMENT_WRONG_FIELD = -7 APPOINTMENT_WRONG_FIELD = -7
APPOINTMENT_INVALID_SIGNATURE = -8 APPOINTMENT_INVALID_SIGNATURE = -8
# Registration errors [-65, -128]
REGISTRATION_MISSING_FIELD = -65
REGISTRATION_WRONG_FIELD_FORMAT = -66
# Custom RPC errors # Custom RPC errors
RPC_TX_REORGED_AFTER_BROADCAST = -98 RPC_TX_REORGED_AFTER_BROADCAST = -98

32
teos/gatekeeper.py Normal file
View File

@@ -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]

View File

@@ -17,6 +17,7 @@ from teos.carrier import Carrier
from teos.inspector import Inspector from teos.inspector import Inspector
from teos.responder import Responder from teos.responder import Responder
from teos.db_manager import DBManager from teos.db_manager import DBManager
from teos.gatekeeper import Gatekeeper
from teos.chain_monitor import ChainMonitor from teos.chain_monitor import ChainMonitor
from teos.block_processor import BlockProcessor from teos.block_processor import BlockProcessor
from teos.tools import can_connect_to_bitcoind, in_correct_network 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 # Fire the API and the ChainMonitor
# FIXME: 92-block-data-during-bootstrap-db # FIXME: 92-block-data-during-bootstrap-db
chain_monitor.monitor_chain() 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: except Exception as e:
logger.error("An error occurred: {}. Shutting down".format(e)) logger.error("An error occurred: {}. Shutting down".format(e))
exit(1) exit(1)

View File

@@ -10,6 +10,7 @@ from teos.watcher import Watcher
from teos.tools import bitcoin_cli from teos.tools import bitcoin_cli
from teos.inspector import Inspector from teos.inspector import Inspector
from teos.responder import Responder from teos.responder import Responder
from teos.gatekeeper import Gatekeeper
from teos.chain_monitor import ChainMonitor from teos.chain_monitor import ChainMonitor
from test.teos.unit.conftest import ( from test.teos.unit.conftest import (
@@ -55,7 +56,9 @@ def run_api(db_manager, carrier, block_processor):
watcher.awake() watcher.awake()
chain_monitor.monitor_chain() 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.daemon = True
api_thread.start() api_thread.start()