From acd72ffa8ae242eb2a54c1ff3221dc79c54588da Mon Sep 17 00:00:00 2001 From: Charles Le Maux Date: Mon, 24 Jun 2024 14:28:46 +0100 Subject: [PATCH 1/5] [+] TODO : firebase_manager.py Signed-off-by: Charles Le Maux --- firebase_manager.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 firebase_manager.py diff --git a/firebase_manager.py b/firebase_manager.py new file mode 100644 index 0000000..10bb4fa --- /dev/null +++ b/firebase_manager.py @@ -0,0 +1,30 @@ +import jwt +import firebase_admin +from firebase_admin import credentials, auth + +cred = credentials.Certificate('servii.json') +firebase_admin.initialize_app(cred) + + +''' +TODO +Write a function that launches upon app's startup, it does check in the firestore for any already running servers. +Fetches the PID's. +Stops all the current processes, +Tell the database they have now stopped running. +Also ensure the program can add an additional argument to avoid this checking for scalability. +''' + +def get_user_from_id(user_id): + return auth.get_user(user_id) + + +def verify_jwt_token(token): + try: + decoded_token = jwt.decode(token, options={"verify_signature": False}) + user_id = decoded_token.get('sub') + return True, user_id + except jwt.ExpiredSignatureError: + return False, None + except jwt.InvalidTokenError: + return False, None From 9ccef0199927155557543ac43d074fa528f188da Mon Sep 17 00:00:00 2001 From: Charles Le Maux Date: Mon, 24 Jun 2024 15:42:41 +0100 Subject: [PATCH 2/5] [+] Added basic firebase authentication checks, and added firestore basic functions. Signed-off-by: Charles Le Maux --- api.py | 25 ++++++++++++++++++----- api_sender.html | 17 ++++++++-------- firebase_manager.py | 49 +++++++++++++++++++++++++++++++++++++++++++-- generic_executor.py | 4 ++++ requirements.txt | 4 ++-- 5 files changed, 82 insertions(+), 17 deletions(-) diff --git a/api.py b/api.py index 0bf0c80..4d87af8 100644 --- a/api.py +++ b/api.py @@ -4,6 +4,7 @@ import inspect from flask import Flask, Response, jsonify, request from flask_cors import CORS +import firebase_manager import generic_executor app = Flask(__name__) @@ -44,6 +45,19 @@ def parse_and_validate_request(parameters: [str]) -> list[str]: data = request.get_json() if not data: raise Exception("Empty request body.") + if 'jwt' not in data: + raise Exception("Missing 'token' in request body. The API doesn't support anonymous access anymore.") + else: + valid, user_id = firebase_manager.verify_jwt_token(data['jwt']) + if not valid: + raise Exception("Invalid JWT token.") + else: + user = firebase_manager.get_user_from_id(user_id) + if not user: + raise Exception("User not found.") + if not user.email_verified: + raise Exception("Your google account isn't verified yet.") + pass for name in parameters: if name not in data: raise Exception(f"Missing parameter {name}") @@ -73,13 +87,14 @@ def dynamic_route_handler(path): route_fn = route_handlers[path] parameters = [] sig = inspect.signature(route_fn) - for param in sig.parameters.values(): parameters.append(param.name) - - mapped_parameters = parse_and_validate_request(parameters) - status: http.HTTPStatus = route_fn(*mapped_parameters) - return generic_response_maker(status) + try : + mapped_parameters = parse_and_validate_request(parameters) + status: http.HTTPStatus = route_fn(*mapped_parameters) + return generic_response_maker(status) + except Exception as e: + return generic_response_maker(http.HTTPStatus.BAD_REQUEST, str(e)) if __name__ == '__main__': diff --git a/api_sender.html b/api_sender.html index 865bc99..76ffd92 100644 --- a/api_sender.html +++ b/api_sender.html @@ -42,6 +42,7 @@ document.addEventListener('DOMContentLoaded', () => { button.addEventListener('click', async event => { const form = event.target.closest('form'); const action = button.dataset.action; + const jwt = "0"; const email = document.getElementById('accountEmail').value; const port = document.getElementById('accountPort').value; const name = document.getElementById('serverName').value; @@ -52,28 +53,28 @@ document.addEventListener('DOMContentLoaded', () => { var data = {} switch(action) { case 'AccountCreate': - data = {email, port} + data = {email, port, jwt} break; case 'AccountDelete': - data = {email, port} + data = {email, port, jwt} break; case 'ServerCreate': - data = {port, name, version} + data = {port, name, version, jwt} break; case 'ServerDelete': - data = {port, name} + data = {port, name, jwt} break; case 'ServerRun': - data = {port, name} + data = {port, name, jwt} break; case 'ServerStop': - data = {port, name} + data = {port, name, jwt} break; case 'UpdateProperty': - data = {port, name, prop, value} + data = {port, name, prop, value, jwt} break; case 'Command': - data = {port, name, command} + data = {port, name, command, jwt} break; } sendRequest(action, data) diff --git a/firebase_manager.py b/firebase_manager.py index 10bb4fa..8c28485 100644 --- a/firebase_manager.py +++ b/firebase_manager.py @@ -1,10 +1,13 @@ +from datetime import datetime + import jwt import firebase_admin -from firebase_admin import credentials, auth +from firebase_admin import credentials, auth, firestore +from google.api_core.exceptions import NotFound, PermissionDenied, Aborted, ResourceExhausted, OutOfRange, DataLoss cred = credentials.Certificate('servii.json') firebase_admin.initialize_app(cred) - +firestore_database = firestore.client() ''' TODO @@ -15,6 +18,7 @@ Tell the database they have now stopped running. Also ensure the program can add an additional argument to avoid this checking for scalability. ''' + def get_user_from_id(user_id): return auth.get_user(user_id) @@ -28,3 +32,44 @@ def verify_jwt_token(token): return False, None except jwt.InvalidTokenError: return False, None + + +def create_firestore(user_id: str, data: dict) -> bool: + doc_ref = firestore_database.collection('users').document(user_id) + try: + doc_ref.create(data) + return True + except (NotFound, PermissionDenied, Aborted, ResourceExhausted, + OutOfRange, DataLoss, TypeError, Exception, ValueError) as e: + log_exception_to_firestore(e, user_id, data) + return False + + +def update_firestore(user_id: str, data: dict) -> bool: + doc_ref = firestore_database.collection('users').document(user_id) + try: + doc_ref.update(data) + return True + except (NotFound, PermissionDenied, Aborted, ResourceExhausted, + OutOfRange, DataLoss, TypeError, Exception, ValueError) as e: + log_exception_to_firestore(e, user_id, data) + return False + + +def log_exception_to_firestore(exception: Exception = None, user_id: str = None, data: dict = None): + new_id: str = datetime.now().strftime('%Y-%m-%d %H:%M:%S %Z%z') + log_entry = { + 'exception_name': str(type(exception).__name__), + 'exception': str(exception) if exception else 'No exception', + 'user_id': str(user_id) if user_id else 'No user_id', + 'data': str(data) if data else 'No data provided', + } + try: + firestore_database.collection('firebase.logs').document(new_id).create(log_entry) + print("Log entry added successfully.") + except Exception as e: + print(f"Failed to add log entry: {e}") + + +if __name__ == "__main__": + pass diff --git a/generic_executor.py b/generic_executor.py index abacf71..9d45646 100644 --- a/generic_executor.py +++ b/generic_executor.py @@ -1,5 +1,6 @@ from server_mc_manager import MinecraftServerManager from http import HTTPStatus + import file_manager mc_manager: MinecraftServerManager = MinecraftServerManager() @@ -90,3 +91,6 @@ def run_command(port: str, command: str) -> HTTPStatus: print(f"Error executing command: {e}") return HTTPStatus.INTERNAL_SERVER_ERROR + +if __name__ == "__main__": + pass diff --git a/requirements.txt b/requirements.txt index 13fc6de..d073e04 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ +firebase_admin==6.5.0 Flask==3.0.3 Flask_Cors==4.0.1 -plotly==5.22.0 -psutil==5.9.8 +PyJWT==2.8.0 From 35689e4dd02566c5fae184d447fcacfc3b4b27dc Mon Sep 17 00:00:00 2001 From: Charles Le Maux Date: Tue, 25 Jun 2024 05:55:49 +0100 Subject: [PATCH 3/5] [++] API:V2 - Firebase support Fully implemented authentication Fully implemented return codes and errors Fully implemented firestore database data Created 'fetch_servers' and 'set_subdomain' routes for front-end use. [+] Security patch, requests will not return anything if not properly authenticated [~] Reworked the route system entirely [+] TODO : firebase_manager.py error handling [+] TODO : global error log using file_manager.log_error(error_type:str, error_message:str) Signed-off-by: Charles Le Maux --- .gitignore | 1 + api.py | 41 +++++++----- api_sender.html | 35 ++++++++--- file_manager.py | 9 +++ firebase_manager.py | 66 +++++++++++++++++-- generic_executor.py | 146 ++++++++++++++++++++++++++++++------------- requirements.txt | 1 + server_mc_manager.py | 62 +++++++++--------- unit_test.py | 6 ++ 9 files changed, 262 insertions(+), 105 deletions(-) diff --git a/.gitignore b/.gitignore index 343b7bc..13d7523 100644 --- a/.gitignore +++ b/.gitignore @@ -165,4 +165,5 @@ cython_debug/ /servers/ /users/ unit_test.py +logs.txt !*.py diff --git a/api.py b/api.py index 4d87af8..1aa9bb3 100644 --- a/api.py +++ b/api.py @@ -1,5 +1,6 @@ import http import inspect +import json from flask import Flask, Response, jsonify, request from flask_cors import CORS @@ -21,7 +22,7 @@ def generic_response_maker(status_code: http.HTTPStatus, _message: str = None) - case http.HTTPStatus.INTERNAL_SERVER_ERROR: message = jsonify({'message': 'Internal Server Error.'}) case http.HTTPStatus.NO_CONTENT: - message = jsonify({'message': 'Deletion successful.'}) + message = jsonify({'message': 'Deletion successful'}) case http.HTTPStatus.ACCEPTED: message = jsonify({'message': 'Action successful.'}) case http.HTTPStatus.BAD_REQUEST: @@ -35,20 +36,16 @@ def generic_response_maker(status_code: http.HTTPStatus, _message: str = None) - case http.HTTPStatus.METHOD_NOT_ALLOWED: message = jsonify({'message': 'This API call does not exist.'}) case _: - status_code = http.HTTPStatus.BAD_GATEWAY - message = jsonify({'message': 'Bad Gateway.'}) + message = jsonify({'message': 'Could not process request.'}) return message, status_code.value -def parse_and_validate_request(parameters: [str]) -> list[str]: - args = [] - data = request.get_json() - if not data: - raise Exception("Empty request body.") - if 'jwt' not in data: +def authenticate_request(data: dict): + if 'token' not in data: raise Exception("Missing 'token' in request body. The API doesn't support anonymous access anymore.") else: - valid, user_id = firebase_manager.verify_jwt_token(data['jwt']) + #valid, user_id = firebase_manager.verify_jwt_token(data['token']) //TODO : REMOVE THIS when the front is ready + valid, user_id = True, data['token'] if not valid: raise Exception("Invalid JWT token.") else: @@ -57,17 +54,27 @@ def parse_and_validate_request(parameters: [str]) -> list[str]: raise Exception("User not found.") if not user.email_verified: raise Exception("Your google account isn't verified yet.") - pass + return user + + +def parse_and_validate_request(parameters: [str]) -> list[str] or None: + args = [] + data = request.get_json() + if not data: + raise Exception("Empty request body.") + user = authenticate_request(data) + data['user'] = user for name in parameters: if name not in data: raise Exception(f"Missing parameter {name}") value = data[name] - if isinstance(value, str): - args.append(value) + args.append(value) return args route_handlers = { + 'SetSubdomain': generic_executor.set_subdomain, + 'FetchServers': generic_executor.fetch_servers, 'AccountCreate': generic_executor.account_create, 'ServerCreate': generic_executor.server_create, 'ServerDelete': generic_executor.server_delete, @@ -91,8 +98,12 @@ def dynamic_route_handler(path): parameters.append(param.name) try : mapped_parameters = parse_and_validate_request(parameters) - status: http.HTTPStatus = route_fn(*mapped_parameters) - return generic_response_maker(status) + if mapped_parameters is None: + return generic_response_maker(http.HTTPStatus.BAD_REQUEST) + status, message = route_fn(*mapped_parameters) + if isinstance(message, list): + return jsonify(message), http.HTTPStatus.OK + return generic_response_maker(status, message if message else None) except Exception as e: return generic_response_maker(http.HTTPStatus.BAD_REQUEST, str(e)) diff --git a/api_sender.html b/api_sender.html index 76ffd92..4e37f0e 100644 --- a/api_sender.html +++ b/api_sender.html @@ -18,6 +18,7 @@ +

Update Property

@@ -33,6 +34,12 @@ +

Set Subdomain

+
+ Command:
+ +
+