vscode / browser_server.py
abcd118q's picture
Create browser_server.py
535332e verified
#!/usr/bin/env python3
import asyncio, json, logging
from typing import Optional, Set
import uvicorn
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from playwright.async_api import async_playwright, Browser, BrowserContext, CDPSession, Page
from playwright_stealth import stealth_async
log = logging.getLogger("browser")
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
app = FastAPI()
_pw = None
browser: Optional[Browser] = None
context: Optional[BrowserContext] = None
page: Optional[Page] = None
cdp: Optional[CDPSession] = None
clients: Set[WebSocket] = set()
VIEWPORT_W = 1280
VIEWPORT_H = 800
async def broadcast(msg: dict):
if not clients: return
text = json.dumps(msg)
dead = set()
for ws in clients:
try: await ws.send_text(text)
except: dead.add(ws)
clients.difference_update(dead)
async def push_nav():
if page:
try:
title = await page.title()
await broadcast({"type": "nav", "url": page.url, "title": title})
except: pass
async def init_browser():
global _pw, browser, context, page, cdp
_pw = await async_playwright().start()
browser = await _pw.chromium.launch(
headless=True,
args=[
"--no-sandbox", "--disable-dev-shm-usage",
"--disable-setuid-sandbox", "--disable-gpu",
"--no-first-run", "--no-default-browser-check",
"--disable-blink-features=AutomationControlled", # stealth
"--disable-background-timer-throttling",
"--disable-renderer-backgrounding",
f"--window-size={VIEWPORT_W},{VIEWPORT_H}",
]
)
context = await browser.new_context(
viewport={"width": VIEWPORT_W, "height": VIEWPORT_H},
user_agent=(
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
),
# Stealth context flags
java_script_enabled=True,
accept_downloads=True,
ignore_https_errors=True,
)
page = await context.new_page()
# ── Apply stealth patches ──────────────────────────────────────
await stealth_async(page)
# Navigation tracking
async def on_nav(frame):
if frame == page.main_frame:
await push_nav()
page.on("framenavigated", on_nav)
# CDP screencast
cdp = await context.new_cdp_session(page)
async def on_frame(params):
await broadcast({"type": "frame", "data": params["data"]})
try:
await cdp.send("Page.screencastFrameAck", {"sessionId": params["sessionId"]})
except: pass
cdp.on("Page.screencastFrame", on_frame)
await cdp.send("Page.startScreencast", {
"format": "jpeg", "quality": 80,
"maxWidth": VIEWPORT_W, "maxHeight": VIEWPORT_H,
"everyNthFrame": 1,
})
await page.goto("https://www.google.com")
log.info("βœ… Stealth browser ready")
@app.on_event("startup")
async def startup(): await init_browser()
@app.websocket("/ws")
async def ws_handler(websocket: WebSocket):
await websocket.accept()
clients.add(websocket)
await push_nav()
try:
while True:
ev = json.loads(await websocket.receive_text())
t = ev.get("type")
if not page: continue
if t == "navigate":
url = ev["url"].strip()
if not url.startswith(("http://", "https://")):
url = ("https://" + url) if ("." in url and " " not in url) \
else f"https://www.google.com/search?q={url}"
await page.goto(url, wait_until="domcontentloaded")
elif t == "back": await page.go_back()
elif t == "forward": await page.go_forward()
elif t == "reload": await page.reload(wait_until="domcontentloaded")
elif t == "click": await page.mouse.click(ev["x"], ev["y"])
elif t == "dblclick":await page.mouse.dblclick(ev["x"], ev["y"])
elif t == "mousemove":await page.mouse.move(ev["x"], ev["y"])
elif t == "mousedown":await page.mouse.down()
elif t == "mouseup": await page.mouse.up()
elif t == "wheel": await page.mouse.wheel(ev.get("dx",0), ev.get("dy",0))
elif t == "keydown":
k = ev["key"]
if ev.get("ctrl"): await page.keyboard.down("Control")
if ev.get("shift"): await page.keyboard.down("Shift")
if ev.get("alt"): await page.keyboard.down("Alt")
await page.keyboard.down(k)
await page.keyboard.up(k)
if ev.get("ctrl"): await page.keyboard.up("Control")
if ev.get("shift"): await page.keyboard.up("Shift")
if ev.get("alt"): await page.keyboard.up("Alt")
elif t == "type": await page.keyboard.type(ev["text"])
except WebSocketDisconnect:
clients.discard(websocket)
except Exception as e:
log.error(f"WS error: {e}")
clients.discard(websocket)
@app.get("/")
async def index():
with open("/app/static/browser.html") as f:
return HTMLResponse(f.read())
app.mount("/static", StaticFiles(directory="/app/static"), name="static")
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8888, log_level="warning")