"""HTTP client for the .NET C2's job endpoints. Stdlib urllib so the blocking calls run off the asyncio loop via to_thread (the event loop belongs to the browser). Each worker points at one job route group — "/jobs" for cs.money, "/skinland/jobs" for skin.land — set once at construction. """ import asyncio import json import logging import urllib.error import urllib.request log = logging.getLogger("c2") class C2Client: def __init__(self, base_url: str, token: str, jobs_path: str): self._base = base_url.rstrip("/") self._token = token self._jobs = jobs_path.strip("/") def _get_job_sync(self): req = urllib.request.Request( f"{self._base}/{self._jobs}/next", headers={"X-Worker-Token": self._token}) try: with urllib.request.urlopen(req, timeout=15) as r: if r.status == 204: return None return json.loads(r.read() or b"null") except urllib.error.HTTPError as e: log.warning("/%s/next -> HTTP %s", self._jobs, e.code) return None except urllib.error.URLError as e: log.warning("C2 unreachable: %s", e) return None def _post_result_sync(self, job_id: str, payload: dict): data = json.dumps(payload).encode() req = urllib.request.Request( f"{self._base}/{self._jobs}/{job_id}/result", data=data, method="POST", headers={"X-Worker-Token": self._token, "Content-Type": "application/json"}) try: with urllib.request.urlopen(req, timeout=60) as r: return json.loads(r.read() or b"null") except urllib.error.HTTPError as e: log.warning("result -> HTTP %s: %r", e.code, e.read()[:200]) return None except urllib.error.URLError as e: log.warning("C2 unreachable posting result: %s", e) return None async def get_job(self): return await asyncio.to_thread(self._get_job_sync) async def post_result(self, job_id, payload): return await asyncio.to_thread(self._post_result_sync, job_id, payload)