From 826d9573d8b40482bd35b49a94088c6bd85b726c Mon Sep 17 00:00:00 2001 From: Johan Koke Date: Thu, 6 Feb 2025 15:15:51 +0000 Subject: [PATCH] first commit --- .gitignore | 1 + README.md | 0 app/docs/index.html | 75 ++++++++++++++++++++++++++++++++ app/docs/style.css | 44 +++++++++++++++++++ app/main.py | 102 ++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yaml | 28 ++++++++++++ dockerfile | 24 +++++++++++ requirements.txt | 5 +++ 8 files changed, 279 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/docs/index.html create mode 100644 app/docs/style.css create mode 100644 app/main.py create mode 100644 docker-compose.yaml create mode 100644 dockerfile create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/app/docs/index.html b/app/docs/index.html new file mode 100644 index 0000000..59c0957 --- /dev/null +++ b/app/docs/index.html @@ -0,0 +1,75 @@ + + + + + + Screenshot API Tester + + + +
+

📸 Screenshot API Tester

+ + + + + + + + + + + + + + + +

Real-Time Updates:

+
+ +

Screenshot:

+ + +
+ + + + diff --git a/app/docs/style.css b/app/docs/style.css new file mode 100644 index 0000000..8f0edfa --- /dev/null +++ b/app/docs/style.css @@ -0,0 +1,44 @@ +body { + font-family: Arial, sans-serif; + background-color: #f4f4f4; + margin: 0; + padding: 0; +} + +.container { + width: 80%; + max-width: 600px; + margin: 20px auto; + background: white; + padding: 20px; + border-radius: 8px; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1); +} + +h1, h3 { + color: #333; +} + +label { + display: block; + margin-top: 10px; +} + +input, select { + width: 100%; + padding: 8px; + margin-top: 5px; +} + +button { + margin-top: 15px; + padding: 10px; + background-color: #0073e6; + color: white; + border: none; + cursor: pointer; +} + +button:hover { + background-color: #005bb5; +} diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..0f56c18 --- /dev/null +++ b/app/main.py @@ -0,0 +1,102 @@ +import hashlib +import logging +import os +import uuid +import redis +import json +from fastapi import FastAPI, Query, HTTPException, WebSocket, WebSocketDisconnect, Request, Depends +from fastapi.responses import FileResponse +from fastapi.staticfiles import StaticFiles +from fastapi_limiter import FastAPILimiter +from fastapi_limiter.depends import RateLimiter +from playwright.async_api import async_playwright + +app = FastAPI() + +# Secret API Key +SECRET_PHRASE = os.getenv("API_SECRET", "my_secret_key") + +# Redis for Rate Limiting +REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379") +redis_client = redis.from_url(REDIS_URL) + +@app.on_event("startup") +async def startup(): + """Initialize FastAPI Limiter.""" + await FastAPILimiter.init(redis_client) + +# Setup Static Files for Documentation +app.mount("/", StaticFiles(directory="app/docs", html=True), name="docs") + +# WebSocket Connections Dictionary +active_connections = {} + +# Device Profiles for Emulation +DEVICE_PROFILES = { + "mobile": {"width": 375, "height": 667, "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)"}, + "tablet": {"width": 768, "height": 1024, "user_agent": "Mozilla/5.0 (iPad; CPU OS 14_0 like Mac OS X)"}, + "desktop": {"width": 1280, "height": 720, "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}, +} + +def generate_md5_hash(url: str, secret: str) -> str: + """Generate MD5 hash for authentication.""" + return hashlib.md5(f"{url}{secret}".encode()).hexdigest() + +async def take_screenshot(websocket: WebSocket, url: str, width: int, height: int, full_height: bool, format: str, click_selectors: list, delay: int, device: str): + """Capture a screenshot using Playwright and return the file path.""" + file_ext = format.lower() + screenshot_path = f"app/docs/screenshots/{uuid.uuid4()}.{file_ext}" + + async with async_playwright() as p: + browser = await p.chromium.launch(headless=True) + await websocket.send_text("Browser launched...") + + viewport = {"width": width, "height": height} + user_agent = None + if device in DEVICE_PROFILES: + viewport = {"width": DEVICE_PROFILES[device]["width"], "height": DEVICE_PROFILES[device]["height"]} + user_agent = DEVICE_PROFILES[device]["user_agent"] + + context = await browser.new_context(viewport=viewport, user_agent=user_agent) + page = await context.new_page() + await page.goto(url, wait_until="load") + await websocket.send_text("Page loaded...") + + for selector in click_selectors: + try: + await page.click(selector, timeout=2000) + await websocket.send_text(f"Clicked element: {selector}") + except Exception: + pass # Ignore if element is not found + + if delay > 0: + await websocket.send_text(f"Waiting for {delay}ms...") + await page.wait_for_timeout(delay) + + if full_height: + height = await page.evaluate("() => document.body.scrollHeight") + await page.set_viewport_size({"width": viewport["width"], "height": height}) + + await page.screenshot(path=screenshot_path, full_page=full_height, type=file_ext) + await websocket.send_text("Screenshot captured.") + + await browser.close() + + return screenshot_path + +@app.websocket("/ws") +async def websocket_endpoint(websocket: WebSocket): + """WebSocket Endpoint for Real-Time Updates.""" + await websocket.accept() + active_connections[websocket] = True + + try: + while True: + data = await websocket.receive_text() + params = json.loads(data) + + screenshot_path = await take_screenshot(websocket, **params) + await websocket.send_text(json.dumps({"screenshot_url": f"/screenshots/{os.path.basename(screenshot_path)}"})) + + except WebSocketDisconnect: + active_connections.pop(websocket, None) diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..9379f87 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,28 @@ +version: "3.8" + +services: + screenshot-api: + build: . + container_name: screenshot-api + ports: + - "80:80" + environment: + - API_SECRET=my_secret_key + - REDIS_URL=redis://redis:6379 + depends_on: + - redis + networks: + - screenshot-network + + redis: + image: redis:alpine + container_name: screenshot-redis + restart: always + ports: + - "6379:6379" + networks: + - screenshot-network + +networks: + screenshot-network: + driver: bridge diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..74742cd --- /dev/null +++ b/dockerfile @@ -0,0 +1,24 @@ +# Use official Python image +FROM python:3.10 + +# Set working directory +WORKDIR /app + +# Copy dependency files first for caching +COPY requirements.txt . + +# Install dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Install Playwright and required browsers +RUN pip install playwright +RUN playwright install --with-deps chromium + +# Copy application files +COPY app ./app + +# Expose ports +EXPOSE 80 + +# Run FastAPI with Uvicorn +CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port 80 --reload"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b993bef --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi +uvicorn +playwright +fastapi-limiter +redis