#!/usr/bin/env python3 """REST client for bettercap API.""" import secrets import string import os from typing import Any, Dict, List, Optional import requests from requests.auth import HTTPBasicAuth class BettercapAPI: def __init__( self, host: str = "127.0.0.1", port: int = 8083, user: str = "admin", password: Optional[str] = None, timeout: int = 10, ): self.host = host self.port = port self.user = user self.password = password or self._generate_password() self.timeout = timeout self._base_url = f"http://{host}:{port}/api" self._auth = HTTPBasicAuth(self.user, self.password) self._session = requests.Session() self._session.auth = self._auth self._session.headers.update({"Content-Type": "application/json"}) @staticmethod def _generate_password(length: int = 32) -> str: alphabet = string.ascii_letters + string.digits return "".join(secrets.choice(alphabet) for _ in range(length)) def _request(self, method: str, endpoint: str, **kwargs) -> requests.Response: url = f"{self._base_url}/{endpoint.lstrip('/')}" kwargs.setdefault("timeout", self.timeout) try: resp = self._session.request(method, url, **kwargs) resp.raise_for_status() return resp except requests.exceptions.ConnectionError: raise ConnectionError(f"Cannot connect to bettercap at {self.host}:{self.port}") except requests.exceptions.Timeout: raise TimeoutError(f"Request to bettercap timed out after {self.timeout}s") def run(self, command: str) -> Dict[str, Any]: resp = self._request("POST", "session", json={"cmd": command}) return resp.json() def get_session(self) -> Dict[str, Any]: resp = self._request("GET", "session") return resp.json() def get_events(self, since: Optional[int] = None) -> List[Dict[str, Any]]: params = {} if since is not None: params["n"] = since resp = self._request("GET", "events", params=params) return resp.json() def is_alive(self) -> bool: try: self.get_session() return True except (ConnectionError, TimeoutError, requests.exceptions.RequestException): return False def start_module(self, module: str) -> Dict[str, Any]: return self.run(f"{module} on") def stop_module(self, module: str) -> Dict[str, Any]: return self.run(f"{module} off") def set_parameter(self, param: str, value: str) -> Dict[str, Any]: # Validate param to prevent injection — allow alphanumeric, dots, underscores if not re.match(r'^[a-zA-Z0-9._-]+$', param): raise ValueError(f"Invalid parameter name: {param}") # Quote value to prevent injection quoted_value = repr(value) return self.run(f"set {param} {quoted_value}") def get_hosts(self) -> List[Dict[str, Any]]: session = self.get_session() return session.get("lan", {}).get("hosts", []) def load_caplet(self, caplet_path: str) -> Dict[str, Any]: # Validate caplet path — must be absolute, must exist, prevent traversal caplet_path = os.path.abspath(caplet_path) if not os.path.isfile(caplet_path): raise FileNotFoundError(f"Caplet not found: {caplet_path}") if not caplet_path.endswith(('.cap', '.caplet')): raise ValueError(f"Invalid caplet extension: {caplet_path}") # Quote path for safety quoted_path = repr(caplet_path) return self.run(f"caplet.load {quoted_path}") @property def credentials(self) -> Dict[str, str]: return {"user": self.user, "password": self.password} @property def api_flags(self) -> List[str]: return [ "-api-rest-address", self.host, "-api-rest-port", str(self.port), "-api-rest-username", self.user, "-api-rest-password", self.password, ]