"""Dependency-free server-side JevShield reference client for Python 3.9+.""" import json import math import queue import socket import threading from urllib.error import HTTPError, URLError from urllib.request import HTTPRedirectHandler, Request, build_opener _ORIGIN = "https://jevshield.com" _MAX_TIMEOUT = 120.0 _MAX_RESPONSE_BYTES = 1_048_576 class JevShieldError(RuntimeError): def __init__(self, message, status=0, code="request_error"): super().__init__(message) self.status = status self.code = code self.error = message class _NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): # Never forward an API key to a redirected origin. return None class JevShieldClient: def __init__(self, api_key, timeout=10): if not isinstance(api_key, str) or not api_key.strip() or "\r" in api_key or "\n" in api_key: raise ValueError("A nonempty server API key is required.") if isinstance(timeout, bool) or not isinstance(timeout, (int, float)) or not math.isfinite(timeout) or not 0 < timeout <= _MAX_TIMEOUT: raise ValueError("timeout must be greater than 0 and at most 120 seconds.") self._api_key = api_key.strip() self._timeout = float(timeout) def check(self, payload): return self._request("POST", "/api/v1/check", payload) def status(self): return self._request("GET", "/api/v1/check") def report(self, payload): return self._request("POST", "/api/v1/report", payload) def _open(self, request, timeout): """Transport seam for local fixtures; production always uses the fixed HTTPS origin.""" return build_opener(_NoRedirect()).open(request, timeout=timeout) def _safe_message(self, value, fallback): if not isinstance(value, str) or not value.strip(): return fallback return value.replace(self._api_key, "[REDACTED]")[:500] def _send(self, request): try: response = self._open(request, self._timeout) except HTTPError as error: response = error with response: status = response.code raw = response.read(_MAX_RESPONSE_BYTES + 1) if len(raw) > _MAX_RESPONSE_BYTES: raise JevShieldError("JevShield response exceeded the client size limit.", status, "invalid_response") try: data = json.loads(raw.decode("utf-8")) except (ValueError, UnicodeError): raise JevShieldError("JevShield returned HTTP %s with an invalid JSON response." % status, status, "invalid_response" if 200 <= status < 300 else "http_error") from None if not 200 <= status < 300: value = data.get("error") if isinstance(data, dict) else None raise JevShieldError(self._safe_message(value, "JevShield returned HTTP %s." % status), status, "http_error") if not isinstance(data, dict): raise JevShieldError("JevShield returned an invalid response object.", status, "invalid_response") return data def _request(self, method, path, payload=None): data = None if method == "POST": if not isinstance(payload, dict): raise JevShieldError("The payload must be a JSON object.", 0, "invalid_payload") try: data = json.dumps(payload, ensure_ascii=False, allow_nan=False, separators=(",", ":")).encode("utf-8") except (ValueError, TypeError, UnicodeError): raise JevShieldError("The payload must be JSON serializable.", 0, "invalid_payload") from None headers = {"Accept": "application/json", "x-api-key": self._api_key, "User-Agent": "JevShield-SDK-Python/1.0"} if data is not None: headers["Content-Type"] = "application/json; charset=utf-8" request = Request(_ORIGIN + path, data=data, headers=headers, method=method) outcomes = queue.Queue(maxsize=1) def execute(): try: outcomes.put((True, self._send(request))) except Exception as error: if isinstance(error, JevShieldError): safe = error elif isinstance(error, (TimeoutError, socket.timeout)) or isinstance(error, URLError) and isinstance(error.reason, (TimeoutError, socket.timeout)): safe = JevShieldError("JevShield request timed out. It may already have consumed quota.", 0, "timeout") else: safe = JevShieldError("JevShield request failed. No automatic retry was attempted.", 0, "network_error") safe.__context__ = None safe.__cause__ = None outcomes.put((False, safe)) # Bound total caller waiting as well as socket inactivity. The transport # may finish after a timeout; do not retry checks blindly. threading.Thread(target=execute, daemon=True).start() try: success, outcome = outcomes.get(timeout=self._timeout) except queue.Empty: raise JevShieldError("JevShield request timed out. It may already have consumed quota.", 0, "timeout") from None if not success: raise outcome from None return outcome