| | from flask import Flask, request, jsonify, send_from_directory |
| | from flask_cors import CORS |
| | import os |
| | import time |
| | import traceback |
| | from pathlib import Path |
| | import threading |
| | import atexit |
| | import random |
| | import math |
| | from PIL import Image, ImageDraw |
| | import requests |
| | import uuid |
| | from datetime import datetime, timedelta |
| | from collections import deque |
| | import string |
| |
|
| | app = Flask(__name__) |
| | CORS(app) |
| |
|
| | BASE_DIR = Path(__file__).parent |
| | PUBLIC_DIR = BASE_DIR / 'public' |
| | PUBLIC_DIR.mkdir(exist_ok=True) |
| |
|
| | PORT = int(os.environ.get('PORT', 7860)) |
| |
|
| | VIEWPORT_WIDTH = 1920 |
| | VIEWPORT_HEIGHT = 1080 |
| |
|
| | VIEWPORT_CONFIG = { |
| | 'width': VIEWPORT_WIDTH, |
| | 'height': VIEWPORT_HEIGHT, |
| | 'device_scale_factor': 1 |
| | } |
| |
|
| | MAX_ROOMS = 5 |
| | ROOM_TIMEOUT_MINUTES = 10 |
| | SCREENSHOT_EXPIRY_MINUTES = 10 |
| | JOB_EXPIRY_MINUTES = 30 |
| |
|
| | SCREENSHOT_BASE_URL = "http://pnode1.danbot.host:1149" |
| |
|
| | class Job: |
| | def __init__(self, job_id, code, lang, base_url): |
| | self.job_id = job_id |
| | self.code = code |
| | self.lang = lang |
| | self.base_url = base_url |
| | self.status = 'queued' |
| | self.result = None |
| | self.error = None |
| | self.created_at = datetime.now() |
| | self.started_at = None |
| | self.completed_at = None |
| | self.room_id = None |
| | self.screenshots = [] |
| | |
| | def to_dict(self): |
| | return { |
| | 'job_id': self.job_id, |
| | 'status': self.status, |
| | 'result': self.result, |
| | 'error': self.error, |
| | 'created_at': self.created_at.isoformat(), |
| | 'started_at': self.started_at.isoformat() if self.started_at else None, |
| | 'completed_at': self.completed_at.isoformat() if self.completed_at else None, |
| | 'room_id': self.room_id, |
| | 'screenshots': self.screenshots |
| | } |
| | |
| | def is_expired(self): |
| | return (datetime.now() - self.created_at) > timedelta(minutes=JOB_EXPIRY_MINUTES) |
| |
|
| | class BrowserRoom: |
| | def __init__(self, room_id): |
| | self.room_id = room_id |
| | self.browser = None |
| | self.context = None |
| | self.page = None |
| | self.cookies = [] |
| | self.created_at = datetime.now() |
| | self.last_activity = datetime.now() |
| | self.is_busy = False |
| | self.current_job_id = None |
| | self.lock = threading.Lock() |
| | self.screenshots_dir = PUBLIC_DIR / f'room_{room_id}' |
| | self.screenshots_dir.mkdir(exist_ok=True) |
| | |
| | def update_activity(self): |
| | self.last_activity = datetime.now() |
| | |
| | def is_expired(self): |
| | return (datetime.now() - self.last_activity) > timedelta(minutes=ROOM_TIMEOUT_MINUTES) |
| | |
| | def cleanup_screenshots(self): |
| | try: |
| | current_time = time.time() |
| | for file in self.screenshots_dir.glob('*.png'): |
| | file_age = current_time - file.stat().st_mtime |
| | if file_age > (SCREENSHOT_EXPIRY_MINUTES * 60): |
| | file.unlink() |
| | print(f"[ROOM-{self.room_id}] Deleted expired screenshot: {file.name}") |
| | except Exception as e: |
| | print(f"[ROOM-{self.room_id}] Screenshot cleanup error: {e}") |
| | |
| | def reset_browser(self): |
| | try: |
| | if self.page: |
| | self.page.close() |
| | self.page = None |
| | if self.context: |
| | try: |
| | self.cookies = self.context.cookies() |
| | print(f"[ROOM-{self.room_id}] Saved {len(self.cookies)} cookies") |
| | except: |
| | pass |
| | self.context.close() |
| | self.context = None |
| | if self.browser: |
| | self.browser.close() |
| | self.browser = None |
| | except Exception as e: |
| | print(f"[ROOM-{self.room_id}] Browser reset error: {e}") |
| | |
| | def cleanup(self): |
| | try: |
| | self.reset_browser() |
| | self.cleanup_screenshots() |
| | if self.screenshots_dir.exists(): |
| | try: |
| | self.screenshots_dir.rmdir() |
| | except: |
| | pass |
| | except Exception as e: |
| | print(f"[ROOM-{self.room_id}] Cleanup error: {e}") |
| |
|
| | class JobManager: |
| | def __init__(self): |
| | self.jobs = {} |
| | self.lock = threading.Lock() |
| | |
| | def generate_job_id(self): |
| | while True: |
| | job_id = ''.join(random.choices(string.ascii_uppercase + string.digits, k=3)) |
| | if job_id not in self.jobs: |
| | return job_id |
| | |
| | def create_job(self, code, lang, base_url): |
| | with self.lock: |
| | job_id = self.generate_job_id() |
| | job = Job(job_id, code, lang, base_url) |
| | self.jobs[job_id] = job |
| | return job |
| | |
| | def get_job(self, job_id): |
| | with self.lock: |
| | return self.jobs.get(job_id) |
| | |
| | def update_job(self, job_id, **kwargs): |
| | with self.lock: |
| | if job_id in self.jobs: |
| | job = self.jobs[job_id] |
| | for key, value in kwargs.items(): |
| | setattr(job, key, value) |
| | |
| | def cleanup_expired_jobs(self): |
| | with self.lock: |
| | expired = [jid for jid, job in self.jobs.items() if job.is_expired()] |
| | for jid in expired: |
| | del self.jobs[jid] |
| | print(f"[JOB-{jid}] Deleted expired job") |
| |
|
| | class RoomManager: |
| | def __init__(self): |
| | self.rooms = {} |
| | self.available_rooms = deque(range(1, MAX_ROOMS + 1)) |
| | self.lock = threading.Lock() |
| | self.cleanup_thread = None |
| | self.stop_cleanup = threading.Event() |
| | |
| | def acquire_room(self, job_id, timeout=300): |
| | start_time = time.time() |
| | while True: |
| | with self.lock: |
| | if self.available_rooms: |
| | room_id = self.available_rooms.popleft() |
| | if room_id not in self.rooms: |
| | self.rooms[room_id] = BrowserRoom(room_id) |
| | room = self.rooms[room_id] |
| | room.is_busy = True |
| | room.current_job_id = job_id |
| | room.update_activity() |
| | print(f"[ROOM-{room_id}] Acquired for job {job_id}. Available rooms: {len(self.available_rooms)}/{MAX_ROOMS}") |
| | return room |
| | |
| | if time.time() - start_time > timeout: |
| | raise Exception(f"Timeout waiting for available room after {timeout}s. All {MAX_ROOMS} rooms are busy.") |
| | |
| | print(f"[QUEUE] All {MAX_ROOMS} rooms busy. Waiting for available room...") |
| | time.sleep(1) |
| | |
| | def release_room(self, room): |
| | with self.lock: |
| | room.is_busy = False |
| | room.current_job_id = None |
| | room.update_activity() |
| | if room.room_id not in self.available_rooms: |
| | self.available_rooms.append(room.room_id) |
| | print(f"[ROOM-{room.room_id}] Released. Available rooms: {len(self.available_rooms)}/{MAX_ROOMS}") |
| | |
| | def cleanup_expired_rooms(self): |
| | while not self.stop_cleanup.is_set(): |
| | try: |
| | current_time = time.time() |
| | |
| | with self.lock: |
| | for room_id, room in list(self.rooms.items()): |
| | if not room.is_busy: |
| | try: |
| | for file in room.screenshots_dir.glob('*.png'): |
| | file_age = current_time - file.stat().st_mtime |
| | if file_age > (SCREENSHOT_EXPIRY_MINUTES * 60): |
| | file.unlink() |
| | print(f"[ROOM-{room_id}] Auto-deleted expired screenshot: {file.name}") |
| | except Exception as e: |
| | print(f"[ROOM-{room_id}] Auto cleanup screenshot error: {e}") |
| | |
| | if not room.is_busy and room.is_expired(): |
| | print(f"[ROOM-{room_id}] Cleaning up expired room") |
| | self.rooms[room_id].cleanup() |
| | del self.rooms[room_id] |
| | |
| | except Exception as e: |
| | print(f"[CLEANUP] Error: {e}") |
| | self.stop_cleanup.wait(60) |
| | |
| | def start_cleanup(self): |
| | self.cleanup_thread = threading.Thread(target=self.cleanup_expired_rooms, daemon=True) |
| | self.cleanup_thread.start() |
| | |
| | def stop_cleanup_thread(self): |
| | self.stop_cleanup.set() |
| | if self.cleanup_thread: |
| | self.cleanup_thread.join(timeout=5) |
| | |
| | def shutdown_all(self): |
| | with self.lock: |
| | for room in self.rooms.values(): |
| | room.cleanup() |
| | self.rooms.clear() |
| | self.available_rooms.clear() |
| |
|
| | room_manager = RoomManager() |
| | job_manager = JobManager() |
| |
|
| | def execute_in_room(code_snippet, room, job_id): |
| | result = {'screenshots': [], 'data': None, 'error': None} |
| | |
| | room.reset_browser() |
| | |
| | browser_obj = None |
| | |
| | try: |
| | print(f"[ROOM-{room.room_id}] [JOB-{job_id}] Starting execution at {time.time()}") |
| | |
| | from camoufox.sync_api import Camoufox |
| | |
| | proxy_server = "http://pnode1.danbot.host:1271" |
| | |
| | browser_obj = Camoufox( |
| | headless=True, |
| | humanize=True, |
| | proxy={'server': proxy_server} |
| | ) |
| | |
| | browser_obj = browser_obj.__enter__() |
| | |
| | if room.cookies: |
| | print(f"[ROOM-{room.room_id}] [JOB-{job_id}] Restoring {len(room.cookies)} cookies") |
| | |
| | viewport_inject_code = """ |
| | import sys |
| | _original_goto = None |
| | |
| | def _patched_goto(self, url, **kwargs): |
| | result = _original_goto(self, url, **kwargs) |
| | try: |
| | self.evaluate(''' |
| | let meta = document.querySelector('meta[name="viewport"]'); |
| | if (!meta) { |
| | meta = document.createElement('meta'); |
| | meta.name = 'viewport'; |
| | document.head.appendChild(meta); |
| | } |
| | meta.content = 'width=1920, height=1080, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'; |
| | document.documentElement.style.width = '1920px'; |
| | document.documentElement.style.height = '1080px'; |
| | document.body.style.width = '1920px'; |
| | document.body.style.height = '1080px'; |
| | ''') |
| | except: |
| | pass |
| | return result |
| | |
| | if 'browser' in dir() and browser is not None: |
| | try: |
| | from playwright.sync_api import Page |
| | if not hasattr(Page, '_viewport_patched'): |
| | _original_goto = Page.goto |
| | Page.goto = _patched_goto |
| | Page._viewport_patched = True |
| | except: |
| | pass |
| | """ |
| | |
| | namespace = { |
| | 'browser': browser_obj, |
| | 'room_cookies': room.cookies, |
| | 'public_dir': str(room.screenshots_dir), |
| | 'room_id': room.room_id, |
| | 'job_id': job_id, |
| | 'time': time, |
| | 'result': result, |
| | 'Path': Path, |
| | 'random': random, |
| | 'math': math, |
| | 'Image': Image, |
| | 'ImageDraw': ImageDraw, |
| | 'requests': requests, |
| | 'print': print, |
| | 'len': len, |
| | 'int': int, |
| | 'str': str, |
| | 'dict': dict, |
| | 'list': list, |
| | 'VIEWPORT_WIDTH': VIEWPORT_WIDTH, |
| | 'VIEWPORT_HEIGHT': VIEWPORT_HEIGHT, |
| | } |
| | |
| | exec(viewport_inject_code, namespace) |
| | |
| | print(f"[ROOM-{room.room_id}] [JOB-{job_id}] Executing code...") |
| | exec(code_snippet, namespace) |
| | |
| | if 'return_value' in namespace: |
| | result['data'] = namespace['return_value'] |
| | |
| | print(f"[ROOM-{room.room_id}] [JOB-{job_id}] Execution completed successfully") |
| | |
| | except Exception as e: |
| | error_msg = str(e) + "\n" + traceback.format_exc() |
| | result['error'] = error_msg |
| | print(f"[ROOM-{room.room_id}] [JOB-{job_id}] ERROR: {error_msg}") |
| | finally: |
| | if browser_obj: |
| | try: |
| | browser_obj.__exit__(None, None, None) |
| | except: |
| | pass |
| | |
| | return result |
| |
|
| | def process_job(job): |
| | room = None |
| | try: |
| | job_manager.update_job(job.job_id, status='running', started_at=datetime.now()) |
| | |
| | room = room_manager.acquire_room(job.job_id, timeout=900) |
| | job_manager.update_job(job.job_id, room_id=room.room_id) |
| | |
| | result = execute_in_room(job.code, room, job.job_id) |
| | |
| | screenshot_files = [] |
| | for file in sorted(room.screenshots_dir.glob('*.png'), key=lambda x: x.stat().st_mtime): |
| | screenshot_files.append({ |
| | 'name': file.name, |
| | 'publicURL': f"{SCREENSHOT_BASE_URL}/files/room_{room.room_id}/{file.name}" |
| | }) |
| | |
| | if result.get('error'): |
| | job_manager.update_job( |
| | job.job_id, |
| | status='failed', |
| | error=result['error'], |
| | completed_at=datetime.now(), |
| | screenshots=screenshot_files |
| | ) |
| | else: |
| | job_manager.update_job( |
| | job.job_id, |
| | status='completed', |
| | result=result.get('data'), |
| | completed_at=datetime.now(), |
| | screenshots=screenshot_files |
| | ) |
| | |
| | except Exception as e: |
| | error_msg = str(e) + "\n" + traceback.format_exc() |
| | job_manager.update_job( |
| | job.job_id, |
| | status='failed', |
| | error=error_msg, |
| | completed_at=datetime.now() |
| | ) |
| | finally: |
| | if room: |
| | room.cleanup_screenshots() |
| | room_manager.release_room(room) |
| |
|
| | @app.route('/api/s-playwright', methods=['POST']) |
| | def execute_playwright(): |
| | try: |
| | data = request.get_json() |
| | |
| | if not data or 'code' not in data or 'lang' not in data: |
| | return jsonify({ |
| | 'success': False, |
| | 'error': 'Missing required fields: code and lang' |
| | }), 400 |
| | |
| | code = data['code'] |
| | lang = data['lang'].lower() |
| | |
| | if lang != 'python': |
| | return jsonify({ |
| | 'success': False, |
| | 'error': f'Only Python is supported, got: {lang}' |
| | }), 400 |
| | |
| | base_url = request.url_root.rstrip('/') |
| | job = job_manager.create_job(code, lang, base_url) |
| | |
| | thread = threading.Thread(target=process_job, args=(job,)) |
| | thread.daemon = True |
| | thread.start() |
| | |
| | return jsonify({ |
| | 'success': True, |
| | 'job_id': job.job_id, |
| | 'status': 'queued', |
| | 'check_url': f'/job/{job.job_id}' |
| | }) |
| | |
| | except Exception as e: |
| | print(f"[API ERROR] {str(e)}\n{traceback.format_exc()}") |
| | return jsonify({ |
| | 'success': False, |
| | 'error': str(e), |
| | 'stack': traceback.format_exc() |
| | }), 500 |
| |
|
| | @app.route('/job/<string:job_id>', methods=['GET']) |
| | def get_job_status(job_id): |
| | job = job_manager.get_job(job_id.upper()) |
| | |
| | if not job: |
| | return jsonify({ |
| | 'success': False, |
| | 'error': 'Job not found' |
| | }), 404 |
| | |
| | return jsonify({ |
| | 'success': True, |
| | 'job': job.to_dict() |
| | }) |
| |
|
| | @app.route('/files/room_<int:room_id>/<path:filename>') |
| | def serve_file(room_id, filename): |
| | room_dir = PUBLIC_DIR / f'room_{room_id}' |
| | return send_from_directory(room_dir, filename) |
| |
|
| | @app.route('/health', methods=['GET']) |
| | def health(): |
| | with room_manager.lock: |
| | busy_rooms = sum(1 for r in room_manager.rooms.values() if r.is_busy) |
| | available = len(room_manager.available_rooms) |
| | |
| | with job_manager.lock: |
| | total_jobs = len(job_manager.jobs) |
| | queued_jobs = sum(1 for j in job_manager.jobs.values() if j.status == 'queued') |
| | running_jobs = sum(1 for j in job_manager.jobs.values() if j.status == 'running') |
| | |
| | return jsonify({ |
| | 'status': 'healthy', |
| | 'rooms': { |
| | 'total': MAX_ROOMS, |
| | 'available': available, |
| | 'busy': busy_rooms |
| | }, |
| | 'jobs': { |
| | 'total': total_jobs, |
| | 'queued': queued_jobs, |
| | 'running': running_jobs |
| | }, |
| | 'viewport': { |
| | 'width': VIEWPORT_WIDTH, |
| | 'height': VIEWPORT_HEIGHT, |
| | 'locked': True |
| | }, |
| | 'timestamp': int(time.time() * 1000) |
| | }) |
| |
|
| | @app.route('/', methods=['GET']) |
| | def index(): |
| | with room_manager.lock: |
| | busy_rooms = sum(1 for r in room_manager.rooms.values() if r.is_busy) |
| | available = len(room_manager.available_rooms) |
| | |
| | return jsonify({ |
| | 'message': 'Multi-Room Camoufox Anti-Detection API with Job System', |
| | 'endpoints': { |
| | 'POST /api/s-playwright': 'Execute camoufox code (returns job_id)', |
| | 'GET /job/:id': 'Check job status (3-character job ID)', |
| | 'GET /health': 'Check API health status' |
| | }, |
| | 'features': [ |
| | f'{MAX_ROOMS} Isolated Browser Rooms', |
| | 'Job Queue System with 3-char IDs', |
| | 'Async Job Processing', |
| | f'Viewport LOCKED at {VIEWPORT_WIDTH}x{VIEWPORT_HEIGHT}', |
| | 'Camoufox Anti-Detection Browser', |
| | 'Human-like Mouse Movement', |
| | 'Advanced Cloudflare WAF Bypass', |
| | 'Auto Room & Job Cleanup', |
| | 'Concurrent Request Protection' |
| | ], |
| | 'configuration': { |
| | 'viewport': {'width': VIEWPORT_WIDTH, 'height': VIEWPORT_HEIGHT, 'locked': True}, |
| | 'port': PORT, |
| | 'rooms': { |
| | 'total': MAX_ROOMS, |
| | 'available': available, |
| | 'busy': busy_rooms |
| | }, |
| | 'timeout': '1200 seconds per execution', |
| | 'room_timeout': f'{ROOM_TIMEOUT_MINUTES} minutes', |
| | 'job_expiry': f'{JOB_EXPIRY_MINUTES} minutes', |
| | 'screenshot_base_url': SCREENSHOT_BASE_URL |
| | } |
| | }) |
| |
|
| | if __name__ == '__main__': |
| | print(f"🚀 Multi-Room Camoufox API with Job System") |
| | print(f"🌐 Port: {PORT}") |
| | print(f"🏠 Browser Rooms: {MAX_ROOMS}") |
| | print(f"📐 Viewport: LOCKED at {VIEWPORT_WIDTH}x{VIEWPORT_HEIGHT}") |
| | print(f"📍 Endpoints: POST /api/s-playwright, GET /job/:id") |
| | print(f"🎭 Features: Job queue, 3-char IDs, Async processing") |
| | print(f"🖱️ Human mouse movement: ENABLED") |
| | print(f"🌍 Proxy: http://pnode1.danbot.host:1271") |
| | print(f"📸 Screenshot URL: {SCREENSHOT_BASE_URL}") |
| | print(f"⏱️ Execution timeout: 1200 seconds") |
| | print(f"🔒 Room timeout: {ROOM_TIMEOUT_MINUTES} minutes") |
| | print(f"📦 Job expiry: {JOB_EXPIRY_MINUTES} minutes") |
| | |
| | room_manager.start_cleanup() |
| | |
| | def cleanup_jobs(): |
| | while True: |
| | job_manager.cleanup_expired_jobs() |
| | time.sleep(300) |
| | |
| | job_cleanup_thread = threading.Thread(target=cleanup_jobs, daemon=True) |
| | job_cleanup_thread.start() |
| | |
| | def cleanup_on_exit(): |
| | room_manager.stop_cleanup_thread() |
| | room_manager.shutdown_all() |
| | |
| | atexit.register(cleanup_on_exit) |
| | |
| | app.run(host='0.0.0.0', port=PORT, debug=False, threaded=True) |