From 9042bae61e6ea002195db49971d84c28c3e43919 Mon Sep 17 00:00:00 2001 From: Charles Le Maux Date: Sat, 7 Sep 2024 16:41:56 +0200 Subject: [PATCH 1/7] [+] Werkzeug utils for filename safety --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6d13e35..01721e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,5 @@ mcipc~=2.4.2 firebase-admin~=6.5.0 protobuf~=4.25.3 typing_extensions~=4.9.0 -APScheduler~=3.10.4 \ No newline at end of file +APScheduler~=3.10.4 +Werkzeug~=3.0.3 \ No newline at end of file From 1123b7f60d164c9592abeec4c176f6fbfb9223b1 Mon Sep 17 00:00:00 2001 From: Charles Le Maux Date: Mon, 9 Sep 2024 23:16:30 +0200 Subject: [PATCH 2/7] [+] Fixed positional argument in Firestore query --- firebase_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/firebase_manager.py b/firebase_manager.py index a97a3c6..6db0a6d 100644 --- a/firebase_manager.py +++ b/firebase_manager.py @@ -6,6 +6,7 @@ import firebase_admin import jwt from firebase_admin import auth, credentials, firestore from google.api_core.exceptions import Aborted, DataLoss, NotFound, OutOfRange, PermissionDenied, ResourceExhausted +from google.cloud.firestore_v1 import FieldFilter import file_manager from generic_executor import mc_manager @@ -51,7 +52,7 @@ def user_field_exists(user_id: str) -> bool: def server_name_taken(user_id: str, server_name: str) -> bool: servers = firestore_database.collection('users').document(user_id).collection('servers') - query = servers.where(field_path='name', op_string='==', value=server_name) + query = servers.where(filter=FieldFilter(field_path='name', op_string='==', value=server_name)) for _ in query.stream(): return True return False From f74d1cf9372b4bf4de68e4a758140a2599d1e8ea Mon Sep 17 00:00:00 2001 From: Charles Le Maux Date: Tue, 10 Sep 2024 00:54:14 +0200 Subject: [PATCH 3/7] [~] Simple async tests --- unit_test.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/unit_test.py b/unit_test.py index 35edac8..bc91385 100644 --- a/unit_test.py +++ b/unit_test.py @@ -1,4 +1,5 @@ import asyncio + import firebase_manager @@ -41,6 +42,3 @@ if __name__ == '__main__': #file_manager.log_action("gqZN3eCHF3V2er3Py3rlgk8u2t83", "test", "DeleteServer") #firebase_manager.set_servers_not_running() asyncio.run(main()) - - - From a7decf8dac925f67818568244b523731969f7a13 Mon Sep 17 00:00:00 2001 From: Charles Le Maux Date: Tue, 10 Sep 2024 01:05:15 +0200 Subject: [PATCH 4/7] [+] File extension parser --- file_manager.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/file_manager.py b/file_manager.py index 71931aa..f31af0c 100644 --- a/file_manager.py +++ b/file_manager.py @@ -86,6 +86,22 @@ def log_action(user_id: str, name: str, action: str, details: str = None): log_error(type(e).__name__, str(e) + " error trying to access history file on not existing server.") +def get_path_from_extension(filename) -> str or None: + allowed_extensions = { + 'zip': "world/datapacks", + 'jar': "plugins" + } + filename_lower = filename.strip().lower() + if '.' not in filename_lower: + return None + + file_extension = filename_lower.split('.')[-1] + if file_extension in allowed_extensions: + return allowed_extensions[file_extension] + else: + return None + + def kebab_to_camel_case(s: str) -> str: parts = s.split('-') return parts[0] + ''.join(part.title() for part in parts[1:]) From c46e04f7f1240ed188352397e38f1ecc63fa1714 Mon Sep 17 00:00:00 2001 From: Charles Le Maux Date: Tue, 10 Sep 2024 01:05:25 +0200 Subject: [PATCH 5/7] [+] Upload route --- app.py | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/app.py b/app.py index 102ea8d..f391183 100644 --- a/app.py +++ b/app.py @@ -1,16 +1,23 @@ +import atexit import http import inspect +import os +from mailbox import FormatError from typing import Union -from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.schedulers.background import BackgroundScheduler +from firebase_admin.auth import UserNotFoundError, UserRecord from flask import (Blueprint, Flask, Response, jsonify, request) from flask_cors import CORS +from werkzeug.datastructures import ImmutableMultiDict, FileStorage +from werkzeug.utils import secure_filename +import file_manager import firebase_manager import generic_executor -import atexit app = Flask(__name__) +app.config['MAX_CONTENT_LENGTH'] = 16 * 1000 * 1000 * 1000 cors = CORS(app, origins="*") apiBP = Blueprint('apiBP', 'BPapi') @@ -28,7 +35,7 @@ def generic_response_maker(status_code: http.HTTPStatus, _message: str = None) - case http.HTTPStatus.ACCEPTED: message = jsonify({'message': 'Action successful.'}) case http.HTTPStatus.BAD_REQUEST: - message = jsonify({'message': 'Bad Request, the property you tried to modify is not valid.'}) + message = jsonify({'message': 'Bad Request.'}) case http.HTTPStatus.NOT_FOUND: message = jsonify({'message': 'Server not found.'}) case http.HTTPStatus.UNSUPPORTED_MEDIA_TYPE: @@ -118,6 +125,59 @@ def dynamic_route_handler(path): return generic_response_maker(http.HTTPStatus.BAD_REQUEST, str(e)) +# [!] This route is specific and has to remain out of the dynamic route handler. +@apiBP.route('/Upload', methods=['POST']) +def upload(): + form = request.form + token: str or None = form.get('token') + name: str or None = form.get('name') + files: ImmutableMultiDict[str, FileStorage] = request.files + + try: + if not form: + raise FormatError(0) + if not files: + raise FormatError(1) + if not token: + raise KeyError('token') + if not name: + raise KeyError('name') + + data: dict = {'token': token} + user: UserRecord = authenticate_request(data) + user_id: str = user.uid + + for _, file in files.items(): + filename = file.filename + internal_path = file_manager.get_path_from_extension(filename) + if internal_path is None: + continue + filename = secure_filename(filename) + file.save(os.path.join(f"users/{user_id}/{name}/{internal_path}", filename)) + + except FormatError as e: + match e: + case 0: + return generic_response_maker(http.HTTPStatus.BAD_REQUEST, "No FormData found in the payload.") + case 1: + return generic_response_maker(http.HTTPStatus.NOT_ACCEPTABLE, "No file(s) were uploaded.") + + except FileNotFoundError as e: + return generic_response_maker(http.HTTPStatus.CONFLICT, "Please launch the server at least once.") + + except KeyError as e: + return f"Missing parameter {e} in FormData.", http.HTTPStatus.BAD_REQUEST + + except UserNotFoundError as e: + return str(e), http.HTTPStatus.BAD_REQUEST + + except Exception as e: + file_manager.log_error(type(e).__name__, str(e)) + return generic_response_maker(http.HTTPStatus.BAD_REQUEST, f"{type(e).__name__}, {str(e)}") + + return generic_response_maker(http.HTTPStatus.OK, "Successfully uploaded files !") + + def exit_safety() -> None: firebase_manager.set_servers_not_running() return From 8a282012e3ee9e48c4e93ae64295d37e3b5f29de Mon Sep 17 00:00:00 2001 From: Charles Le Maux Date: Tue, 10 Sep 2024 01:05:36 +0200 Subject: [PATCH 6/7] [+] Updated testing page --- api_sender.html | 156 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 138 insertions(+), 18 deletions(-) diff --git a/api_sender.html b/api_sender.html index bc6255c..8a02693 100644 --- a/api_sender.html +++ b/api_sender.html @@ -2,17 +2,76 @@ - API Interaction Form + File Upload and API Interaction Form + -

Generic Calls

+

File Upload Form

+
+ + +
+ +
+ +

API Interaction Form

- Email:
- Port:
- Name:
- Version:
- Framework:
+ Email:
+ Port:
+ Name:
+ Version:
+ Framework:
@@ -22,34 +81,84 @@ -

Update Property

- Property:
- Value:
+ Property:
+ Value:

Send Command

- Command:
+ Command:

Set Subdomain

-
- Command:
- + + Subdomain:
+
From 2fe31596c0e0f94ff659c8acf2e5919eb3201598 Mon Sep 17 00:00:00 2001 From: Charles Le Maux Date: Tue, 10 Sep 2024 01:06:56 +0200 Subject: [PATCH 7/7] [+] Corrected exception handling syntax --- app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.py b/app.py index f391183..1c97743 100644 --- a/app.py +++ b/app.py @@ -162,7 +162,7 @@ def upload(): case 1: return generic_response_maker(http.HTTPStatus.NOT_ACCEPTABLE, "No file(s) were uploaded.") - except FileNotFoundError as e: + except FileNotFoundError: return generic_response_maker(http.HTTPStatus.CONFLICT, "Please launch the server at least once.") except KeyError as e: