mirror of
https://github.com/aljazceru/python-teos.git
synced 2025-12-17 14:14:22 +01:00
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:
93
teos/api.py
93
teos/api.py
@@ -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"]),
|
||||||
|
|||||||
@@ -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
32
teos/gatekeeper.py
Normal 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]
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user