Files
python-teos/teos/api.py
Sergi Delgado Segura 83b3913cb5 Adds basic authentication logic.
+ Users need to be registered in order to send appointments (free registration for now)
+ The tower gives them a number of appointments to work with
+ Non-registered users and users with no enough appoitnemnts slots return the same error (to avoid proving)

- Authentication does not cover get_* requests yet
- No tests
- No docs
2020-03-25 17:13:35 +01:00

281 lines
11 KiB
Python

import os
import logging
from math import ceil
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,
ENCRYPTED_BLOB_MAX_SIZE_HEX,
)
# ToDo: #5-add-async-to-api
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:
# FIXME: DOCS
"""
The :class:`API` is in charge of the interface between the user and the tower. It handles and server user requests.
Args:
inspector (:obj:`Inspector <teos.inspector.Inspector>`): an ``Inspector`` instance to check the correctness of
the received data.
watcher (:obj:`Watcher <teos.watcher.Watcher>`): a ``Watcher`` instance to pass the requests to.
"""
# TODO: UNITTEST
def __init__(self, inspector, watcher, gatekeeper):
self.inspector = inspector
self.watcher = watcher
self.gatekeeper = gatekeeper
# TODO: UNITTEST, DOCS
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
# FIXME: UNITTEST
def add_appointment(self):
"""
Main endpoint of the Watchtower.
The client sends requests (appointments) to this endpoint to request a job to the Watchtower. Requests must be
json encoded and contain an ``appointment`` field and optionally a ``signature`` and ``public_key`` fields.
Returns:
: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 <teos.errors>`.
"""
# Getting the real IP if the server is behind a reverse proxy
remote_addr = get_remote_addr()
logger.info("Received add_appointment request", from_addr="{}".format(remote_addr))
if request.is_json:
# Check content type once if properly defined
request_data = request.get_json()
rcode, message = self.gatekeeper.identify_user(
request_data.get("appointment"), request_data.get("signature")
)
if rcode:
rcode = HTTP_BAD_REQUEST
error = "appointment rejected. Error {}: {}".format(rcode, message)
return jsonify({"error": error}), rcode
else:
user_pk = message
appointment = self.inspector.inspect(request_data.get("appointment"))
if type(appointment) == Appointment:
# An appointment will fill 1 slot per ENCRYPTED_BLOB_MAX_SIZE_HEX block.
required_slots = ceil(len(appointment.encrypted_blob.data) / ENCRYPTED_BLOB_MAX_SIZE_HEX)
if self.gatekeeper.get_slots(user_pk) >= required_slots:
appointment_added, signature = self.watcher.add_appointment(appointment)
if appointment_added:
rcode = HTTP_OK
response = {"locator": appointment.locator, "signature": signature}
self.gatekeeper.fill_subscription_slots(user_pk, required_slots)
else:
rcode = HTTP_SERVICE_UNAVAILABLE
error = "appointment rejected"
response = {"error": error}
else:
rcode = errors.APPOINTMENT_INVALID_SIGNATURE_OR_INSUFFICIENT_SLOTS
error = "invalid signature or the user does not have enough slots available"
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 = {"error": error}
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
def get_appointment(self):
"""
Gives information about a given appointment state in the Watchtower.
The information is requested by ``locator``.
Returns:
:obj:`dict`: A json formatted dictionary containing information about the requested appointment.
A ``status`` flag is added to the data provided by either the :obj:`Watcher <teos.watcher.Watcher>` or the
:obj:`Responder <teos.responder.Responder>` that signals the status of the appointment.
- Appointments hold by the :obj:`Watcher <teos.watcher.Watcher>` are flagged as ``being_watched``.
- Appointments hold by the :obj:`Responder <teos.responder.Responder>` are flagged as ``dispute_triggered``.
- Unknown appointments are flagged as ``not_found``.
"""
# Getting the real IP if the server is behind a reverse proxy
remote_addr = get_remote_addr()
locator = request.args.get("locator")
response = []
logger.info("Received get_appointment request", from_addr="{}".format(remote_addr), locator=locator)
# ToDo: #15-add-system-monitor
if not isinstance(locator, str) or len(locator) != LOCATOR_LEN_HEX:
response.append({"locator": locator, "status": "not_found"})
return jsonify(response)
locator_map = self.watcher.db_manager.load_locator_map(locator)
triggered_appointments = self.watcher.db_manager.load_all_triggered_flags()
if locator_map is not None:
for uuid in locator_map:
if uuid not in triggered_appointments:
appointment_data = self.watcher.db_manager.load_watcher_appointment(uuid)
if appointment_data is not None:
appointment_data["status"] = "being_watched"
response.append(appointment_data)
tracker_data = self.watcher.db_manager.load_responder_tracker(uuid)
if tracker_data is not None:
tracker_data["status"] = "dispute_responded"
response.append(tracker_data)
else:
response.append({"locator": locator, "status": "not_found"})
response = jsonify(response)
return response
def get_all_appointments(self):
"""
Gives information about all the appointments in the Watchtower.
This endpoint should only be accessible by the administrator. Requests are only allowed from localhost.
Returns:
:obj:`dict`: A json formatted dictionary containing all the appointments hold by the
:obj:`Watcher <teos.watcher.Watcher>` (``watcher_appointments``) and by the
:obj:`Responder <teos.responder.Responder>` (``responder_trackers``).
"""
# ToDo: #15-add-system-monitor
response = None
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()
response = jsonify({"watcher_appointments": watcher_appointments, "responder_trackers": responder_trackers})
else:
abort(404)
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"]),
}
for url, params in routes.items():
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"
app.run(host=HOST, port=PORT)