[+] Upload route

New upload route for the servii API
Now it only allows the user to send jar and zip files as this:
- Jar files are considered as plugins and are stored in the plugins folder of the server
- zip files are considered as datapacks and are therefore stored in the datapacks folder within the defaults world folder on the server
This commit is contained in:
charleslemaux 2024-09-10 01:10:31 +02:00 committed by GitHub
commit 29aa5a40e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 222 additions and 26 deletions

View File

@ -2,17 +2,76 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>API Interaction Form</title> <title>File Upload and API Interaction Form</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #f0f0f0;
}
form {
display: flex;
flex-direction: column;
gap: 15px;
background-color: #ffffff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
input[type="file"], input[type="text"], input[type="number"] {
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
button {
padding: 12px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #0056b3;
}
.message {
margin-top: 10px;
padding: 12px;
border-radius: 4px;
}
.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
</style>
</head> </head>
<body> <body>
<h2>Generic Calls</h2> <h2>File Upload Form</h2>
<form id="uploadForm">
<input type="file" id="files" accept=".zip,.jar,.txt" multiple required>
<button type="submit">Upload File</button>
</form>
<div id="message" class="message"></div>
<h2>API Interaction Form</h2>
<form id="genericForm"> <form id="genericForm">
Email: <label for="accountEmail"></label><input type="text" id="accountEmail"><br> Email: <input type="text" id="accountEmail"><br>
Port: <label for="accountPort"></label><input type="number" id="accountPort"><br> Port: <input type="number" id="accountPort"><br>
Name: <label for="serverName"></label><input type="text" id="serverName"><br> Name: <input type="text" id="serverName"><br>
Version: <label for="serverVersion"></label><input type="text" id="serverVersion"><br> Version: <input type="text" id="serverVersion"><br>
Framework: <label for="serverFramework"></label><input type="text" id="serverFramework"><br> Framework: <input type="text" id="serverFramework"><br>
<button type="button" class="actionButton" data-action="AccountCreate">Create Account</button> <button type="button" class="actionButton" data-action="AccountCreate">Create Account</button>
<button type="button" class="actionButton" data-action="AccountDelete">Delete Account</button> <button type="button" class="actionButton" data-action="AccountDelete">Delete Account</button>
<button type="button" class="actionButton" data-action="ServerCreate">Create Server</button> <button type="button" class="actionButton" data-action="ServerCreate">Create Server</button>
@ -22,34 +81,84 @@
<button type="button" class="actionButton" data-action="FetchServers">Fetch Servers</button> <button type="button" class="actionButton" data-action="FetchServers">Fetch Servers</button>
<button type="button" class="actionButton" data-action="FetchLogs">Fetch Logs</button> <button type="button" class="actionButton" data-action="FetchLogs">Fetch Logs</button>
<button type="button" class="actionButton" data-action="FetchPlayersStatus">Fetch Players Status</button> <button type="button" class="actionButton" data-action="FetchPlayersStatus">Fetch Players Status</button>
</form> </form>
<h2>Update Property</h2> <h2>Update Property</h2>
<form id="updatePropertyForm"> <form id="updatePropertyForm">
Property: <label for="update_property"></label><input type="text" id="update_property"><br> Property: <input type="text" id="update_property"><br>
Value: <label for="update_value"></label><input type="text" id="update_value"><br> Value: <input type="text" id="update_value"><br>
<button type="button" class="actionButton" data-action="UpdateProperties">Update Property</button> <button type="button" class="actionButton" data-action="UpdateProperties">Update Property</button>
</form> </form>
<h2>Send Command</h2> <h2>Send Command</h2>
<form id="sendCommandForm"> <form id="sendCommandForm">
Command: <label for="command"></label><input type="text" id="command"><br> Command: <input type="text" id="command"><br>
<button type="button" class="actionButton" data-action="Command">Send command</button> <button type="button" class="actionButton" data-action="Command">Send command</button>
</form> </form>
<h2>Set Subdomain</h2> <h2>Set Subdomain</h2>
<form id="sendCommandForm"> <form id="setSubdomainForm">
Command: <label for="subdomain"></label><input type="text" id="subdomain"><br> Subdomain: <input type="text" id="subdomain"><br>
<button type="button" class="actionButton" data-action="SetSubdomain">Send command</button> <button type="button" class="actionButton" data-action="SetSubdomain">Set Subdomain</button>
</form> </form>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const uploadForm = document.getElementById('uploadForm');
const genericForm = document.getElementById('genericForm');
const updatePropertyForm = document.getElementById('updatePropertyForm');
const sendCommandForm = document.getElementById('sendCommandForm');
const setSubdomainForm = document.getElementById('setSubdomainForm');
const messageDiv = document.getElementById('message');
// File Upload functionality
uploadForm.addEventListener('submit', async event => {
event.preventDefault();
const fileInput = document.getElementById('files');
const filesLength = fileInput.files.length;
if (filesLength === 0) {
showMessage('Please select at least one file.', 'error');
return;
}
const formData = new FormData();
formData.append('name', 'local');
formData.append('token', 'MpkbDMOO8PQddQgB5VgBQdTMWF53');
for (let i = 0; i < filesLength; i++) {
formData.append(`${i}`, fileInput.files[i]);
}
try {
const response = await fetch('http://localhost:3000/Upload', {
method: 'POST',
body: formData
});
if (!response.ok) {
const errorMessage = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(errorMessage, 'text/html');
const title = doc.querySelector('title')?.textContent || 'Unknown Error';
const description = doc.querySelector('p')?.textContent || 'Unable to determine error description.';
showMessage(`${title}: ${description}`, 'error');
} else {
const result = await response.json();
showMessage(`File uploaded successfully. Filename: ${result.filename}`, 'success');
}
} catch (error) {
console.error('Error:', error);
showMessage('An error occurred. Please try again.', 'error');
}
});
// API Interaction functionality
const buttons = document.querySelectorAll('.actionButton'); const buttons = document.querySelectorAll('.actionButton');
buttons.forEach(button => { buttons.forEach(button => {
button.addEventListener('click', async event => { button.addEventListener('click', async event => {
event.preventDefault();
const action = button.dataset.action; const action = button.dataset.action;
const token = "MpkbDMOO8PQddQgB5VgBQdTMWF53"; const token = "MpkbDMOO8PQddQgB5VgBQdTMWF53";
const framework = document.getElementById('serverFramework').value; const framework = document.getElementById('serverFramework').value;
@ -63,6 +172,7 @@ document.addEventListener('DOMContentLoaded', () => {
const command = document.getElementById('command').value; const command = document.getElementById('command').value;
const props = [[prop, value], ["max-players", "666"]]; const props = [[prop, value], ["max-players", "666"]];
let data = {}; let data = {};
switch(action) { switch(action) {
case 'FetchServers': case 'FetchServers':
data = {token}; data = {token};
@ -98,13 +208,14 @@ document.addEventListener('DOMContentLoaded', () => {
data = {port, name, command, token}; data = {port, name, command, token};
break; break;
case 'SetSubdomain': case 'SetSubdomain':
data = {token, subdomain} data = {token, subdomain};
break; break;
} }
sendRequest(action, data) sendRequest(action, data)
.then(response => response.text()) .then(response => response.text())
.then(data => alert(`Response: ${data}`)) .then(data => alert(`Response: ${data}`))
.catch(error => console.error('Error:', error)); .catch(error => console.error('Error:', error));
}); });
}); });
@ -117,6 +228,15 @@ document.addEventListener('DOMContentLoaded', () => {
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
} }
function showMessage(message, type) {
messageDiv.textContent = message;
messageDiv.className = `message ${type}`;
setTimeout(() => {
messageDiv.textContent = '';
messageDiv.className ='message';
}, 5000);
}
}); });
</script> </script>

66
app.py
View File

@ -1,16 +1,23 @@
import atexit
import http import http
import inspect import inspect
import os
from mailbox import FormatError
from typing import Union 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 import (Blueprint, Flask, Response, jsonify, request)
from flask_cors import CORS from flask_cors import CORS
from werkzeug.datastructures import ImmutableMultiDict, FileStorage
from werkzeug.utils import secure_filename
import file_manager
import firebase_manager import firebase_manager
import generic_executor import generic_executor
import atexit
app = Flask(__name__) app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 16 * 1000 * 1000 * 1000
cors = CORS(app, origins="*") cors = CORS(app, origins="*")
apiBP = Blueprint('apiBP', 'BPapi') apiBP = Blueprint('apiBP', 'BPapi')
@ -28,7 +35,7 @@ def generic_response_maker(status_code: http.HTTPStatus, _message: str = None) -
case http.HTTPStatus.ACCEPTED: case http.HTTPStatus.ACCEPTED:
message = jsonify({'message': 'Action successful.'}) message = jsonify({'message': 'Action successful.'})
case http.HTTPStatus.BAD_REQUEST: 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: case http.HTTPStatus.NOT_FOUND:
message = jsonify({'message': 'Server not found.'}) message = jsonify({'message': 'Server not found.'})
case http.HTTPStatus.UNSUPPORTED_MEDIA_TYPE: 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)) 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:
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: def exit_safety() -> None:
firebase_manager.set_servers_not_running() firebase_manager.set_servers_not_running()
return return

View File

@ -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.") 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: def kebab_to_camel_case(s: str) -> str:
parts = s.split('-') parts = s.split('-')
return parts[0] + ''.join(part.title() for part in parts[1:]) return parts[0] + ''.join(part.title() for part in parts[1:])

View File

@ -6,6 +6,7 @@ import firebase_admin
import jwt import jwt
from firebase_admin import auth, credentials, firestore from firebase_admin import auth, credentials, firestore
from google.api_core.exceptions import Aborted, DataLoss, NotFound, OutOfRange, PermissionDenied, ResourceExhausted from google.api_core.exceptions import Aborted, DataLoss, NotFound, OutOfRange, PermissionDenied, ResourceExhausted
from google.cloud.firestore_v1 import FieldFilter
import file_manager import file_manager
from generic_executor import mc_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: def server_name_taken(user_id: str, server_name: str) -> bool:
servers = firestore_database.collection('users').document(user_id).collection('servers') 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(): for _ in query.stream():
return True return True
return False return False

View File

@ -10,4 +10,5 @@ mcipc~=2.4.2
firebase-admin~=6.5.0 firebase-admin~=6.5.0
protobuf~=4.25.3 protobuf~=4.25.3
typing_extensions~=4.9.0 typing_extensions~=4.9.0
APScheduler~=3.10.4 APScheduler~=3.10.4
Werkzeug~=3.0.3

View File

@ -1,4 +1,5 @@
import asyncio import asyncio
import firebase_manager import firebase_manager
@ -41,6 +42,3 @@ if __name__ == '__main__':
#file_manager.log_action("gqZN3eCHF3V2er3Py3rlgk8u2t83", "test", "DeleteServer") #file_manager.log_action("gqZN3eCHF3V2er3Py3rlgk8u2t83", "test", "DeleteServer")
#firebase_manager.set_servers_not_running() #firebase_manager.set_servers_not_running()
asyncio.run(main()) asyncio.run(main())