""" backend/routes/internet_routes.py §2.4 — Internet Layer: live OSINT, web search, URL fetch, scrape """ from fastapi import HTTPException, APIRouter, Depends from pydantic import BaseModel from typing import Optional from backend.billing.gating import require_feature from backend.billing.plans import Feature router = APIRouter() # v22: OSINT is a paid feature in the plan catalog (plus/pro), WEB_SEARCH is not (free # grants it). So the gate goes on /osint specifically, NOT on the router — gating the # whole router would take /search away from the free tier that is entitled to it. _OSINT_GATE = Depends(require_feature(Feature.OSINT)) class SearchRequest(BaseModel): query: str num_results: int = 5 persona: Optional[str] = "jarvis" class FetchRequest(BaseModel): url: str summarise: bool = True class OsintRequest(BaseModel): target: str # domain, IP, username, email, etc. recon_type: str = "full" # full | whois | social | dns | email @router.post("/search") async def web_search(req: SearchRequest): """Live OSINT web search via the backend tool registry.""" try: from backend.tools.web_search_tools import search_web results = await search_web(req.query, num_results=req.num_results) return {"status": "ok", "query": req.query, "results": results} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/fetch") async def fetch_url(req: FetchRequest): """Fetch and optionally summarise a URL using the browser tool.""" try: # browser_tools has never exported `fetch_url` (it exposes navigate / # get_ephemeral_context), so this route was a guaranteed 500: # "cannot import name 'fetch_url' from 'backend.tools.browser_tools'". # The function that actually does this job — Playwright fetch, CAPTCHA # handling, text extraction — is web_search_tools.fetch_page. from backend.tools.web_search_tools import fetch_page content = await fetch_page(req.url) return {"status": "ok", "url": req.url, "content": content} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/osint") async def osint_recon(req: OsintRequest, _user: str = _OSINT_GATE): """ Elite Defensive OSINT recon endpoint. Performs web reconnaissance, digital footprint analysis, and threat modelling on a given target. """ if not req.target: raise HTTPException(status_code=400, detail="target is required") try: from backend.tools.web_search_tools import search_web queries = { "full": [f'site:shodan.io "{req.target}"', f'"{req.target}" breach OR leak OR hack', f'"{req.target}" whois DNS records'], "whois": [f'whois "{req.target}"'], "social": [f'"{req.target}" site:twitter.com OR site:linkedin.com OR site:github.com'], "dns": [f'DNS records "{req.target}"'], "email": [f'"{req.target}" email breach haveibeenpwned'], }.get(req.recon_type, [f'"{req.target}"']) all_results = [] for q in queries: results = await search_web(q, num_results=3) # search_web returns {"results": [...]} (or {"error": ...}). # extend()ing the dict itself appended its KEYS — so findings came # back as ["results", "results", ...] instead of the actual hits. if isinstance(results, dict): all_results.extend(results.get("results", [])) elif isinstance(results, list): all_results.extend(results) return { "status": "ok", "target": req.target, "recon_type": req.recon_type, "findings": all_results, } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/status") async def internet_status(): """Health check: verify the internet layer is reachable.""" import httpx try: async with httpx.AsyncClient(timeout=5) as client: r = await client.get("https://www.google.com") return {"status": "online", "latency_ms": r.elapsed.total_seconds() * 1000} except Exception as e: raise HTTPException(status_code=503, detail=f"Internet layer offline: {e}")