diff --git a/client/src/components/PasswordRequirementsComponent.tsx b/client/src/components/PasswordRequirementsComponent.tsx
new file mode 100644
index 0000000..2cd1fe9
--- /dev/null
+++ b/client/src/components/PasswordRequirementsComponent.tsx
@@ -0,0 +1,51 @@
+/*
+ * PasswordRequirementsComponent.tsx - Display password requirements for a password input.
+ * Copyright (C) 2024, Kieran Gordon
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import React from 'react';
+
+interface PasswordRequirementsProps {
+ password: string;
+}
+
+const PasswordRequirementsComponent: React.FC = ({ password }) => {
+ const requirements = [
+ { regex: /.{8,}/, label: 'At least 8 characters' },
+ { regex: /[A-Z]/, label: 'At least 1 uppercase letter' },
+ { regex: /[a-z]/, label: 'At least 1 lowercase letter' },
+ { regex: /[0-9].*[0-9]/, label: 'At least 2 digits' },
+ { regex: /[!@#$%^&*(),.?":{}|<>]/, label: 'At least 1 symbol' },
+ { regex: /^\S*$/, label: 'No spaces' },
+ ];
+
+ return (
+
+ {requirements.map((requirement, index) => (
+
+ {requirement.label}
+
+ ))}
+
+ );
+};
+
+export default PasswordRequirementsComponent;
\ No newline at end of file
diff --git a/client/src/screens/LoginRegistrationScreen.tsx b/client/src/screens/LoginRegistrationScreen.tsx
index 45bf393..e718e1f 100644
--- a/client/src/screens/LoginRegistrationScreen.tsx
+++ b/client/src/screens/LoginRegistrationScreen.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import passwordValidator from "password-validator";
+import "bootstrap-icons/font/bootstrap-icons.css";
import { FC, ReactElement, useEffect, useState } from "react";
import {
Alert,
@@ -27,6 +27,7 @@ import {
Form,
Modal,
Row,
+ Card,
} from "react-bootstrap";
import { useNavigate } from "react-router-dom";
import validator from "validator";
@@ -39,6 +40,7 @@ import {
} from "../api/AccountsAPI";
import Footer from "../components/FooterComponent";
import NavbarComponent from "../components/NavbarComponent";
+import PasswordRequirementsComponent from "../components/PasswordRequirementsComponent";
const LoginRegistrationScreen: FC = (): ReactElement => {
const [loginUsername, setLoginUsername] = useState("");
@@ -46,6 +48,8 @@ const LoginRegistrationScreen: FC = (): ReactElement => {
const [registerUsername, setRegisterUsername] = useState("");
const [registerEmail, setRegisterEmail] = useState("");
const [registerPassword, setRegisterPassword] = useState("");
+ const [registerPasswordMatch, setRegisterPasswordMatch] = useState("");
+ const [showPassword, setShowPassword] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const [showModal, setShowModal] = useState(false);
const [token, setToken] = useState("");
@@ -53,7 +57,6 @@ const LoginRegistrationScreen: FC = (): ReactElement => {
const [twoFactorMessage, setTwoFactorMessage] = useState(""); // Error message for 2FA
const [twoFactorCode, setTwoFactorCode] = useState("");
const [emailMessage, setEmailMessage] = useState(""); // Error message for email verification
- const schema = new passwordValidator();
const navigate = useNavigate();
useEffect(() => {
@@ -114,6 +117,11 @@ const LoginRegistrationScreen: FC = (): ReactElement => {
return;
}
+ if (registerPassword !== registerPasswordMatch) {
+ setErrorMessage("Passwords do not match.");
+ return;
+ }
+
// Username can only contain letters, numbers, underscores, and dashes. It cannot contain spaces.
if (!validator.matches(registerUsername, /^[a-zA-Z0-9_-]+$/)) {
setErrorMessage(
@@ -123,26 +131,11 @@ const LoginRegistrationScreen: FC = (): ReactElement => {
}
// Minimum length 8, maximum length 100, must have uppercase, must have lowercase, must have 2 digits, must not have spaces
- schema
- .is()
- .min(8)
- .is()
- .max(100)
- .has()
- .uppercase()
- .has()
- .lowercase()
- .has()
- .digits(2)
- .has()
- .not()
- .spaces()
- .has()
- .symbols();
-
- if (!schema.validate(registerPassword)) {
+ if (
+ !validator.matches(registerPassword, /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d.*\d)(?!.*\s).{8,100}$/)
+ ) {
setErrorMessage(
- "Invalid password. Your password must be at least 8 characters long, have at least 1 uppercase letter, have at least 1 lowercase letter, have 1 symbol, have at least 2 digits, and must not have spaces."
+ "Invalid password. Your password must be at least 8 characters long, contain at least 1 uppercase letter, contain at least 1 lowercase letter, contain at least 2 digits, and not contain spaces."
);
return;
}
@@ -204,132 +197,150 @@ const LoginRegistrationScreen: FC = (): ReactElement => {
};
return (
-
+
-
Login
-
Already have an account? Login.
-
- {errorMessage}
-
-
-
- setLoginUsername(e.target.value)}
- />
-
-
-
-
- setLoginPassword(e.target.value)}
- />
-
-
-
-
-
-
-
-
+
+
+
Login
+
Login to your account.
+
+
+ setLoginUsername(e.target.value)}
+ />
+
+
+
+
+ setLoginPassword(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
Register
-
New to Buffet? Register for an account.
-
- Rules for usernames and passwords
-
-
-
- Usernames can only contain letters, numbers, underscores, and
- dashes. It cannot contain spaces.
-
-
- Passwords must be at least 8 characters long, have at least 1
- uppercase letter, have at least 1 lowercase letter, have 1
- symbol, have at least 2 digits, and must not have spaces.
-
);
};
diff --git a/client/src/screens/UserManagementScreen.tsx b/client/src/screens/UserManagementScreen.tsx
index 80623df..ca63d0f 100644
--- a/client/src/screens/UserManagementScreen.tsx
+++ b/client/src/screens/UserManagementScreen.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import passwordValidator from "password-validator";
+import "bootstrap-icons/font/bootstrap-icons.css";
import { FC, ReactElement, useContext, useEffect, useState } from "react";
import {
Alert,
@@ -44,12 +44,12 @@ import {
import Footer from "../components/FooterComponent";
import NavbarComponent from "../components/NavbarComponent";
import { AuthContext } from "../contexts/AuthContext";
+import PasswordRequirementsComponent from "../components/PasswordRequirementsComponent";
const UserManagementScreen: FC = (): ReactElement => {
const authContext = useContext(AuthContext);
const user = authContext?.user;
const navigate = useNavigate();
- const schema = new passwordValidator();
const [getEmail, setCurrentEmail] = useState("");
const [getUserName, setCurrentUserName] = useState("");
@@ -62,6 +62,7 @@ const UserManagementScreen: FC = (): ReactElement => {
const [warningMessage, setWarningMessage] = useState("");
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showTwoFactorModal, setShowTwoFactorModal] = useState(false);
+ const [showPassword, setShowPassword] = useState(false);
const [qrCode, setQrCode] = useState("");
const [twoFactorCode, setTwoFactorCode] = useState("");
const [showDisableTwoFactorModal, setShowDisableTwoFactorModal] = useState(false);
@@ -114,24 +115,11 @@ const UserManagementScreen: FC = (): ReactElement => {
}
// Minimum length 8, maximum length 100, must have uppercase, must have lowercase, must have 2 digits, must not have spaces
- schema
- .is()
- .min(8)
- .max(100)
- .has()
- .uppercase()
- .has()
- .lowercase()
- .has()
- .digits(2)
- .has()
- .symbols()
- .not()
- .spaces();
-
- if (!schema.validate(newPassword)) {
+ if (
+ !validator.matches(newPassword, /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d.*\d)(?!.*\s).{8,100}$/)
+ ) {
setWarningMessage(
- "Invalid password. Your password must be at least 8 characters long, have at least 1 uppercase letter, have at least 1 lowercase letter, have 1 symbol, have at least 2 digits, and must not have spaces."
+ "Invalid password. Your password must be at least 8 characters long, contain at least one uppercase letter, contain at least one lowercase letter, contain at least two digits, and not contain any spaces."
);
return;
}
@@ -342,7 +330,7 @@ const UserManagementScreen: FC = (): ReactElement => {
setCurrentPassword(e.target.value)}
/>
@@ -681,6 +690,9 @@ const UserManagementScreen: FC = (): ReactElement => {
+
diff --git a/client/nginx/example.conf b/nginx/nginx.conf
similarity index 97%
rename from client/nginx/example.conf
rename to nginx/nginx.conf
index 4823e7e..3483dbe 100644
--- a/client/nginx/example.conf
+++ b/nginx/nginx.conf
@@ -15,7 +15,7 @@ server {
}
location /api/ {
- proxy_pass https://localhost:8000;
+ proxy_pass http:/localhost:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
diff --git a/server/app.py b/server/app.py
index 2729677..a5add84 100644
--- a/server/app.py
+++ b/server/app.py
@@ -65,8 +65,8 @@
# Create default user in user table called 'admin' with password 'admin' and email 'admin@admin.com'
# This is for testing purposes only and should be removed in production
if not Users.query.filter_by(username="admin").first():
- hashed_password = generate_password_hash("admin")
- admin = Users(username="admin", email="admin@admin.com", password=hashed_password, role="admin")
+ hashed_password = generate_password_hash("admin").decode("utf-8")
+ admin = Users(username="admin", email="admin@admin.com", password=hashed_password[:80], role="admin")
db.session.add(admin)
db.session.commit()
diff --git a/server/config.py b/server/config.py
index 71266c4..f8527da 100644
--- a/server/config.py
+++ b/server/config.py
@@ -69,8 +69,10 @@ class ApplicationConfig:
SQLALCHEMY_ECHO = os.environ.get("SQLALCHEMY_ECHO") # Echo SQL queries to the console
SQLALCHEMY_TRACK_MODIFICATIONS = os.environ.get("SQLALCHEMY_TRACK_MODIFICATIONS") # Track modifications
+ WEBSOCKET_SSL_ENABLED = os.environ.get("WEBSOCKET_SSL_ENABLED") # SSL enabled
+ GUNICORN_SSL_ENABLED = os.environ.get("GUNICORN_SSL_ENABLED") # SSL enabled
+
SSL_CERTIFICATE_PATH = os.environ.get("SSL_CERTIFICATE_PATH") # Certificate path
- SSL_ENABLED = os.environ.get("SSL_ENABLED") # SSL enabled
SSL_KEY_PATH = os.environ.get("SSL_KEY_PATH") # Key path
RATE_LIMIT = os.environ.get("RATE_LIMIT") # Rate limit
@@ -85,8 +87,8 @@ class ApplicationConfig:
LDAP_BIND_USER_DN = os.environ.get("LDAP_BIND_USER_DN") # LDAP bind user DN
LDAP_BIND_USER_PASSWORD = os.environ.get("LDAP_BIND_USER_PASSWORD") # LDAP bind user password
- VM_PORT_START = os.environ.get("VM_PORT_START") # Starting port for virtual machines
- WEBSOCKET_PORT_START = os.environ.get("WEBSOCKET_PORT_START") # Starting port for websockets
+ VM_PORT_START = os.environ.get("VM_PORT_START") # VM port start
+ WEBSOCKET_PORT_START = os.environ.get("WEBSOCKET_PORT_START") # Websocket port start
@staticmethod
def get_config():
diff --git a/server/gunicorn.conf.py b/server/gunicorn.conf.py
index 058d85c..d380647 100644
--- a/server/gunicorn.conf.py
+++ b/server/gunicorn.conf.py
@@ -21,11 +21,15 @@
bind = os.getenv("GUNICORN_BIND_ADDRESS") # Set the bind address. This can be overridden using --bind.
-certfile = os.getenv("SSL_CERTIFICATE_PATH") # Set the certificate file. This is required for HTTPS.
-keyfile = os.getenv("SSL_KEY_PATH") # Set the key file. This is required for HTTPS.
-workers = os.cpu_count() * 2 + 1 # Auto-calculate the number of workers. This can be overridden using --workers.
+if os.getenv("GUNICORN_SSL_ENABLED", "false").lower() == "true":
+ certfile = os.getenv("SSL_CERTIFICATE_PATH") # Set the certificate file. This is required for HTTPS.
+ keyfile = os.getenv("SSL_KEY_PATH") # Set the key file. This is required for HTTPS.
+else:
+ certfile = None
+ keyfile = None
+workers = int(os.getenv("GUNICORN_WORKERS", os.cpu_count() * 2 + 1)) # Auto-calculate the number of workers. This can be overridden using --workers.
# This is set to 2 * the number of CPU cores + 1 for optimal performance.
-threads = os.cpu_count() * 2 + 1 # Auto-calculate the number of threads. This can be overridden using --threads.
+threads = int(os.getenv("GUNICORN_THREADS", os.cpu_count() * 2 + 1)) # Auto-calculate the number of threads. This can be overridden using --threads.
# This is set to 2 * the number of CPU cores + 1 for optimal performance.
worker_class = os.getenv("GUNICORN_WORKER_CLASS") # Set the worker class. This can be overridden using --worker-class.
loglevel = os.getenv("GUNICORN_LOG_LEVEL") # Set the log level. This can be overridden using --log-level.
diff --git a/server/models.py b/server/models.py
index 9c5c406..e9bd375 100644
--- a/server/models.py
+++ b/server/models.py
@@ -106,7 +106,7 @@ class VirtualMachines(db.Model):
id = db.Column(db.Integer, primary_key=True)
port = db.Column(db.Integer, nullable=False)
- wsport = db.Column(db.Integer, nullable=False)
+ websocket_port = db.Column(db.Integer, nullable=False)
iso = db.Column(db.String(80), nullable=False)
websockify_process_id = db.Column(db.Integer, nullable=False)
process_id = db.Column(db.Integer, nullable=False)
@@ -161,8 +161,10 @@ class ApplicationConfigDb(db.Model):
SQLALCHEMY_ECHO = db.Column(db.Boolean, nullable=True)
SQLALCHEMY_TRACK_MODIFICATIONS = db.Column(db.Boolean, nullable=True)
+ WEBSOCKET_SSL_ENABLED = db.Column(db.Boolean, nullable=True)
+ GUNICORN_SSL_ENABLED = db.Column(db.Boolean, nullable=True)
+
SSL_CERTIFICATE_PATH = db.Column(db.String(255), nullable=True)
- SSL_ENABLED = db.Column(db.Boolean, nullable=True)
SSL_KEY_PATH = db.Column(db.String(255), nullable=True)
RATE_LIMIT = db.Column(db.String(255), nullable=True)
diff --git a/server/routes/config_endpoints.py b/server/routes/config_endpoints.py
index 24ef461..664b2c9 100644
--- a/server/routes/config_endpoints.py
+++ b/server/routes/config_endpoints.py
@@ -53,3 +53,8 @@ def update_config():
else:
config_entry = ApplicationConfigDb(key=key, value=value)
config_entry.save()
+
+ # Update the environment variable
+ ApplicationConfig.__dict__[key] = value
+
+ return jsonify({"message": "Config updated"})
diff --git a/server/routes/vm_endpoints.py b/server/routes/vm_endpoints.py
index afef33a..ca2d791 100644
--- a/server/routes/vm_endpoints.py
+++ b/server/routes/vm_endpoints.py
@@ -60,111 +60,6 @@ def get_hardware_platform():
return os.uname().machine
-@vm_endpoints.route("/api/vm/iso/", methods=["GET"])
-@jwt_required()
-def index_vm():
- """Index the ISO files
-
- Returns:
- json: Index of the ISO files with logos
- """
-
- # Get the user from the authorization token
- user = Users.query.filter_by(id=get_jwt_identity()).first()
- if not user:
- return jsonify({"message": "Invalid user"}), 401
-
- with open(f"{ApplicationConfig.ISO_DIR}/index.json", "r", encoding="utf-8") as f:
- data = json.load(f)
- for iso in data:
- logo_path = f"{ApplicationConfig.ISO_DIR}/logos/{iso['logo']}"
- if os.path.exists(logo_path):
- with open(logo_path, "rb") as f:
- iso["logo"] = base64.b64encode(f.read()).decode("utf-8")
- else:
- with open("assets/unknown.png", "rb") as f:
- iso["logo"] = base64.b64encode(f.read()).decode("utf-8")
-
- return jsonify(data), 200
-
-
-@vm_endpoints.route("/api/vm/create/", methods=["POST"])
-@jwt_required()
-async def create_vm():
- """Create a virtual machine
-
- Returns:
- json: Virtual machine
- """
- user = Users.query.filter_by(id=get_jwt_identity()).first()
- if not user:
- return jsonify({"message": "Invalid user"}), 401
-
- data = request.get_json()
- if not data or "iso" not in data:
- return jsonify({"message": "Invalid data format"}), 400
-
- iso = data["iso"]
-
- # Get the ISO architecture
- arch = get_iso_architecture(iso)
- if not arch:
- return jsonify({"message": "Invalid ISO"}), 404
-
- port_int = find_available_port()
- if port_int is None:
- return jsonify({"message": "The server is at maximum capacity. Please try again later."}), 500
-
- wsport, port = port_int + int(ApplicationConfig.WEBSOCKET_PORT_START), port_int + int(ApplicationConfig.VM_PORT_START)
-
- # Check if user already has a virtual machine
- if VirtualMachines.query.filter_by(user_id=user.id).count() > 0:
- return jsonify({
- "message": "Users may only have one virtual machine at a time. Please shut down your current virtual machine before creating a new one."
- }), 403
-
- try:
- create_log_directory(user.id)
- iso_dir = f"{ApplicationConfig.ISO_DIR}/{iso}"
- validate_iso(iso_dir)
-
- # Start the virtual machine process
- process_id = start_vm_process(arch, iso_dir, port_int, user.id)
-
- # If the host OS is not macOS, setup QMP and VNC password
- password = None
- if get_host_os_type() != "Darwin":
- # Wait for VM to start
- await asyncio.sleep(2)
-
- # Setup QMP and VNC password
- qmp = await setup_qmp_client(user.id)
- password = create_random_vnc_password()
- await qmp.execute("set_password", {"protocol": "vnc", "password": password})
-
- # Start websockify process
- websockify_process_id = start_websockify(wsport, port)
-
- except subprocess.CalledProcessError:
- return jsonify({"message": "Critical error creating virtual machine. Please try again later."}), 500
-
- # Create the VM in the database
- new_vm = VirtualMachines(
- port=port,
- wsport=wsport,
- iso=iso,
- websockify_process_id=websockify_process_id,
- process_id=process_id,
- user_id=user.id,
- log_file=f"{datetime.now().strftime('%H:%M:%S')}-{iso}.pcap",
- vnc_password=password,
- )
- db.session.add(new_vm)
- db.session.commit()
-
- return jsonify({"id": new_vm.id, "wsport": wsport, "iso": iso, "user_id": user.id}), 201
-
-
def get_iso_architecture(iso):
"""Retrieve the architecture of a given ISO."""
with open(f"{ApplicationConfig.ISO_DIR}/index.json", "r", encoding="utf-8") as f:
@@ -179,7 +74,7 @@ def find_available_port():
"""Find the next available VM port."""
max_count = int(ApplicationConfig.MAX_VM_COUNT)
for port_int in range(max_count):
- if not VirtualMachines.query.filter_by(port=port_int + int(ApplicationConfig.WEBSOCKET_PORT_START)).first():
+ if not VirtualMachines.query.filter_by(port=port_int + int(ApplicationConfig.VM_PORT_START)).first():
return port_int
return None
@@ -201,8 +96,6 @@ def start_vm_process(arch, iso_dir, port_int, user_id):
"""Start the virtual machine process."""
command = [
f"qemu-system-{arch}",
- "-monitor",
- "stdio",
"-m",
f"{ApplicationConfig.MAX_VM_MEMORY}M",
"-smp",
@@ -232,7 +125,6 @@ def start_vm_process(arch, iso_dir, port_int, user_id):
# If KVM is enabled, add the KVM flag
if ApplicationConfig.KVM_ENABLED:
command.extend(["-enable-kvm", "-cpu", "host"])
-
# Add HVF accelerator if running on macOS with an M series chip, and ISO is ARM64
if get_host_os_type() == "Darwin" and get_hardware_platform() == "arm64" and arch == "aarch64":
# get the latest version of qemu by searching for the latest version in the directory
@@ -259,12 +151,12 @@ async def setup_qmp_client(user_id):
return qmp
-def start_websockify(wsport, port):
+def start_websockify(websocket_port, port):
"""Start the websockify process."""
client_url = ApplicationConfig.CLIENT_URL
api_url = socket.gethostbyname(socket.gethostname())
- if ApplicationConfig.SSL_ENABLED:
+ if ApplicationConfig.WEBSOCKET_SSL_ENABLED:
cert_path = ApplicationConfig.SSL_CERTIFICATE_PATH
key_path = ApplicationConfig.SSL_KEY_PATH
@@ -275,14 +167,119 @@ def start_websockify(wsport, port):
"--key",
key_path,
"--ssl-only",
- f"{client_url}:{wsport}",
+ f"{client_url}:{websocket_port}",
f"{api_url}:{port}",
])
else:
- process = subprocess.Popen(["websockify", f"{client_url}:{wsport}", f"{api_url}:{port}"])
+ process = subprocess.Popen(["websockify", f"{client_url}:{websocket_port}", f"{api_url}:{port}"])
return process.pid
+@vm_endpoints.route("/api/vm/iso/", methods=["GET"])
+@jwt_required()
+def index_vm():
+ """Index the ISO files
+
+ Returns:
+ json: Index of the ISO files with logos
+ """
+
+ # Get the user from the authorization token
+ user = Users.query.filter_by(id=get_jwt_identity()).first()
+ if not user:
+ return jsonify({"message": "Invalid user"}), 401
+
+ with open(f"{ApplicationConfig.ISO_DIR}/index.json", "r", encoding="utf-8") as f:
+ data = json.load(f)
+ for iso in data:
+ logo_path = f"{ApplicationConfig.ISO_DIR}/logos/{iso['logo']}"
+ if os.path.exists(logo_path):
+ with open(logo_path, "rb") as f:
+ iso["logo"] = base64.b64encode(f.read()).decode("utf-8")
+ else:
+ with open("assets/unknown.png", "rb") as f:
+ iso["logo"] = base64.b64encode(f.read()).decode("utf-8")
+
+ return jsonify(data), 200
+
+
+@vm_endpoints.route("/api/vm/create/", methods=["POST"])
+@jwt_required()
+async def create_vm():
+ """Create a virtual machine
+
+ Returns:
+ json: Virtual machine
+ """
+ user = Users.query.filter_by(id=get_jwt_identity()).first()
+ if not user:
+ return jsonify({"message": "Invalid user"}), 401
+
+ data = request.get_json()
+ if not data or "iso" not in data:
+ return jsonify({"message": "Invalid data format"}), 400
+
+ iso = data["iso"]
+
+ # Get the ISO architecture
+ arch = get_iso_architecture(iso)
+ if not arch:
+ return jsonify({"message": "Invalid ISO"}), 404
+
+ port_int = find_available_port()
+ if port_int is None:
+ return jsonify({"message": "The server is at maximum capacity. Please try again later."}), 500
+
+ websocket_port, port = port_int + int(ApplicationConfig.WEBSOCKET_PORT_START), port_int + int(ApplicationConfig.VM_PORT_START)
+
+ # Check if user already has a virtual machine
+ if VirtualMachines.query.filter_by(user_id=user.id).count() > 0:
+ return jsonify({
+ "message": "Users may only have one virtual machine at a time. Please shut down your current virtual machine before creating a new one."
+ }), 403
+
+ try:
+ create_log_directory(user.id)
+ iso_dir = f"{ApplicationConfig.ISO_DIR}/{iso}"
+ validate_iso(iso_dir)
+
+ # Start the virtual machine process
+ process_id = start_vm_process(arch, iso_dir, port_int, user.id)
+
+ # If the host OS is not macOS, setup QMP and VNC password
+ password = None
+ if get_host_os_type() != "Darwin":
+ # Wait for VM to start
+ await asyncio.sleep(2)
+
+ # Setup QMP and VNC password
+ qmp = await setup_qmp_client(user.id)
+ password = create_random_vnc_password()
+ await qmp.execute("set_password", {"protocol": "vnc", "password": password})
+
+ # Start websockify process
+ websockify_process_id = start_websockify(websocket_port, port)
+
+ except subprocess.CalledProcessError:
+ return jsonify({"message": "Critical error creating virtual machine. Please try again later."}), 500
+
+ # Create the VM in the database
+ new_vm = VirtualMachines(
+ port=port,
+ websocket_port=websocket_port,
+ iso=iso,
+ websockify_process_id=websockify_process_id,
+ process_id=process_id,
+ user_id=user.id,
+ log_file=f"{datetime.now().strftime('%H:%M:%S')}-{iso}.pcap",
+ vnc_password=password,
+ )
+ db.session.add(new_vm)
+ db.session.commit()
+
+ return jsonify({"id": new_vm.id, "websocket_port": websocket_port, "iso": iso, "user_id": user.id}), 201
+
+
@vm_endpoints.route("/api/vm/delete/", methods=["DELETE"])
@jwt_required()
def delete_vm():
@@ -365,7 +362,7 @@ def get_user_vm():
return (
jsonify({
"id": vm.id,
- "wsport": vm.wsport,
+ "websocket_port": vm.websocket_port,
"iso": vm.iso,
"user_id": vm.user_id,
"name": name,
@@ -408,7 +405,7 @@ def get_vm_by_id():
return jsonify({"message": "You can only get your own virtual machine"}), 403
return (
- jsonify({"id": vm.id, "wsport": vm.wsport, "iso": vm.iso, "user_id": vm.user_id}),
+ jsonify({"id": vm.id, "websocket_port": vm.websocket_port, "iso": vm.iso, "user_id": vm.user_id}),
200,
)