import json from edge.alert import USER_AGENT, sign class ServerClient: def __init__(self, base_url: str, device_id: str, secret: str, nonce_fn, now_fn, http_get=None, http_post=None, timeout: float = 5.0): self.base_url = base_url.rstrip("/") self.device_id = device_id self.secret = secret self.nonce_fn = nonce_fn self.now_fn = now_fn self.timeout = timeout if http_get is None or http_post is None: import requests http_get = http_get or requests.get http_post = http_post or requests.post self._get = http_get self._post = http_post def _headers(self, body: str) -> dict: nonce = self.nonce_fn() ts = str(self.now_fn()) # User-Agent: cafe24's WAF 403s the default python-requests UA (see edge/alert.py). return {"User-Agent": USER_AGENT, "X-Nonce": nonce, "X-Timestamp": ts, "X-Signature": sign(self.secret, nonce, ts, body)} def fetch_config(self) -> dict | None: url = f"{self.base_url}/api/config?device_id={self.device_id}" try: resp = self._get(url, headers=self._headers(""), timeout=self.timeout, allow_redirects=False) except Exception: return None if 200 <= resp.status_code < 300: try: return resp.json() except Exception: return None return None def send_heartbeat(self, metrics: dict) -> bool: payload = {"device_id": self.device_id, **metrics} body = json.dumps(payload, sort_keys=True) headers = {"Content-Type": "application/json", **self._headers(body)} try: resp = self._post(f"{self.base_url}/api/heartbeat", data=body, headers=headers, timeout=self.timeout, allow_redirects=False) except Exception: return False return 200 <= resp.status_code < 300