diff --git a/.gitignore b/.gitignore index 6599fc4..af3e958 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -known_hosts \ No newline at end of file +known_hosts +__pycache__/ +*.py[cod] +*.egg-info/ \ No newline at end of file diff --git a/cross_py_builder/build_agent.py b/cross_py_builder/build_agent.py index 7a6e5f3..4d892e6 100755 --- a/cross_py_builder/build_agent.py +++ b/cross_py_builder/build_agent.py @@ -9,6 +9,7 @@ import time from flask import Flask, request, jsonify, send_file import os +import re import zipfile import subprocess import glob @@ -46,26 +47,28 @@ def update_files(): error_files = [] for key in request.files: uploaded_file = request.files[key] - if uploaded_file.filename: - original_filename = uploaded_file.filename - temp_save_path = os.path.join(LOCAL_DIR, f"{original_filename}.tmp") - uploaded_file.save(temp_save_path) - if os.path.getsize(temp_save_path): - try: - backup_path = os.path.join(LOCAL_DIR, original_filename + ".old") - local_file_path = os.path.join(LOCAL_DIR, original_filename) - os.rename(local_file_path, backup_path) - shutil.move(temp_save_path, local_file_path) - os.remove(backup_path) - needs_install_requirements |= (requirements_path == local_file_path) - updated_files.append(original_filename) - except Exception as e: - print(f"Exception updating file ({original_filename}): {e}") - error_files.append(original_filename) - else: - print(f"Invalid size for {temp_save_path}!") + original_filename = os.path.basename(uploaded_file.filename or "") + if not original_filename: + error_files.append(uploaded_file.filename) + continue + temp_save_path = os.path.join(LOCAL_DIR, f"{original_filename}.tmp") + uploaded_file.save(temp_save_path) + if os.path.getsize(temp_save_path): + try: + backup_path = os.path.join(LOCAL_DIR, original_filename + ".old") + local_file_path = os.path.join(LOCAL_DIR, original_filename) + os.rename(local_file_path, backup_path) + shutil.move(temp_save_path, local_file_path) + os.remove(backup_path) + needs_install_requirements |= (requirements_path == local_file_path) + updated_files.append(original_filename) + except Exception as e: + print(f"Exception updating file ({original_filename}): {e}") error_files.append(original_filename) - os.remove(temp_save_path) + else: + print(f"Invalid size for {temp_save_path}!") + error_files.append(original_filename) + os.remove(temp_save_path) if os.path.exists(requirements_path) and needs_install_requirements: print(f"\nInstalling Required Packages...") @@ -159,28 +162,47 @@ def status(): def generate_job_id(): return str(uuid.uuid4()).split('-')[-1] +JOB_ID_PATTERN = re.compile(r"[0-9a-f]{12}") + +def is_valid_job_id(job_id): + return bool(job_id) and bool(JOB_ID_PATTERN.fullmatch(job_id)) + @app.route("/checkout_git", methods=['POST']) def checkout_project(): - start_time = datetime.datetime.now() - repo_url = request.json.get('repo_url') - if not repo_url: + if not request.is_json or not request.json.get('repo_url'): return jsonify({'error': 'Repository URL is required'}), 400 + repo_url = request.json['repo_url'] print(f"\n========== Checking Out Git Project ==========") job_id = generate_job_id() - repo_dir = os.path.join(TMP_DIR, job_id) + build_root = os.path.join(TMP_DIR, BUILD_DIR) + repo_dir = os.path.join(build_root, job_id) + start_time = datetime.datetime.now() try: + os.makedirs(build_root, exist_ok=True) system_status['status'] = "cloning_repo" subprocess.check_call(['git', 'clone', repo_url, repo_dir]) system_status['status'] = "ready" - except subprocess.CalledProcessError as e: - print(f"Error cloning repo: {e}") + return install_and_build(repo_dir, job_id, start_time) + except Exception as e: + print(f"Error processing checkout: {e}") system_status['status'] = "ready" - return jsonify({'error': 'Failed to clone repository'}), 500 + system_status['running_job'] = None + shutil.rmtree(repo_dir, ignore_errors=True) + if isinstance(e, subprocess.CalledProcessError): + return jsonify({'error': 'Failed to clone repository'}), 500 + return jsonify({'error': f"Uncaught error processing checkout: {e}"}), 500 - return install_and_build(repo_dir, job_id, start_time) +def safe_extract(zip_ref, dest_dir): + """Extract a zipfile, rejecting members that would escape dest_dir.""" + dest_dir = os.path.realpath(dest_dir) + for member in zip_ref.infolist(): + member_path = os.path.realpath(os.path.join(dest_dir, member.filename)) + if os.path.commonpath([dest_dir, member_path]) != dest_dir: + raise ValueError(f"Unsafe path in archive: {member.filename!r}") + zip_ref.extract(member, dest_dir) @app.route('/upload', methods=['POST']) def upload_project(): @@ -204,12 +226,13 @@ def upload_project(): # Extract ZIP with zipfile.ZipFile(zip_path, 'r') as zip_ref: print(f"Extracting uploaded project zip...") - zip_ref.extractall(working_dir) + safe_extract(zip_ref, working_dir) return install_and_build(working_dir, job_id, start_time) except Exception as e: print(f"Uncaught error processing job: {e}") system_status['status'] = "ready" + shutil.rmtree(working_dir, ignore_errors=True) return jsonify({"error": f"Uncaught error processing job: {e}"}), 500 def install_and_build(project_path, job_id, start_time): @@ -238,7 +261,7 @@ def install_and_build(project_path, job_id, start_time): print(f"Error setting up virtual environment: {e}") system_status['status'] = "ready" system_status['running_job'] = None - os.rmdir(project_path) + shutil.rmtree(project_path, ignore_errors=True) return jsonify({"error": f"Error setting up virtual environment: {e}"}), 500 # Install requirements @@ -255,7 +278,7 @@ def install_and_build(project_path, job_id, start_time): print(f"Error installing requirements: {e}") system_status['status'] = "ready" system_status['running_job'] = None - os.rmdir(project_path) + shutil.rmtree(project_path, ignore_errors=True) return jsonify({"error": f"Error installing requirements: {e}"}), 500 results = {} @@ -291,10 +314,7 @@ def install_and_build(project_path, job_id, start_time): print(f"Error compiling project: {e}") system_status['status'] = "ready" system_status['running_job'] = None - try: - os.remove(project_path) - except PermissionError: - pass + shutil.rmtree(project_path, ignore_errors=True) return jsonify({"error": f"Error compiling project: {e}"}), 500 dist_path = os.path.join(project_path, "dist") @@ -331,6 +351,9 @@ def system_os(): def download_binaries(job_id): """Handles downloading the compiled PyInstaller binaries for a given job.""" try: + if not is_valid_job_id(job_id): + return jsonify({"error": f"Invalid job id: {job_id}"}), 404 + # Locate the build directory job_path = os.path.join(TMP_DIR, BUILD_DIR, job_id) dist_path = os.path.join(job_path, "dist") @@ -359,6 +382,9 @@ def download_binaries(job_id): @app.route('/delete/', methods=['GET']) def delete_project(job_id): + if not is_valid_job_id(job_id): + return jsonify({"error": f"Invalid job id: {job_id}"}), 404 + job_path = os.path.join(TMP_DIR, BUILD_DIR, job_id) if not os.path.exists(job_path):