mirror of
https://github.com/infinition/Bjorn.git
synced 2026-03-15 08:52:00 +00:00
- Implemented LokiUtils class with GET and POST endpoints for managing scripts, jobs, and payloads. - Added SentinelUtils class with GET and POST endpoints for managing events, rules, devices, and notifications. - Both classes include error handling and JSON response formatting.
104 lines
3.9 KiB
Python
104 lines
3.9 KiB
Python
"""
|
|
Bifrost — Bettercap REST API client.
|
|
Ported from pwnagotchi/bettercap.py using urllib (no requests dependency).
|
|
"""
|
|
import json
|
|
import logging
|
|
import base64
|
|
import urllib.request
|
|
import urllib.error
|
|
|
|
from logger import Logger
|
|
|
|
logger = Logger(name="bifrost.bettercap", level=logging.DEBUG)
|
|
|
|
|
|
class BettercapClient:
|
|
"""Synchronous REST client for the bettercap API."""
|
|
|
|
def __init__(self, hostname='127.0.0.1', scheme='http', port=8081,
|
|
username='user', password='pass'):
|
|
self.hostname = hostname
|
|
self.scheme = scheme
|
|
self.port = port
|
|
self.username = username
|
|
self.password = password
|
|
self.url = "%s://%s:%d/api" % (scheme, hostname, port)
|
|
self.websocket = "ws://%s:%s@%s:%d/api" % (username, password, hostname, port)
|
|
self._auth_header = 'Basic ' + base64.b64encode(
|
|
('%s:%s' % (username, password)).encode()
|
|
).decode()
|
|
|
|
def _request(self, method, path, data=None, verbose_errors=True):
|
|
"""Make an HTTP request to bettercap API."""
|
|
url = "%s%s" % (self.url, path)
|
|
body = json.dumps(data).encode() if data else None
|
|
req = urllib.request.Request(url, data=body, method=method)
|
|
req.add_header('Authorization', self._auth_header)
|
|
if body:
|
|
req.add_header('Content-Type', 'application/json')
|
|
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
raw = resp.read().decode('utf-8')
|
|
try:
|
|
return json.loads(raw)
|
|
except json.JSONDecodeError:
|
|
return raw
|
|
except urllib.error.HTTPError as e:
|
|
err = "error %d: %s" % (e.code, e.read().decode('utf-8', errors='replace').strip())
|
|
if verbose_errors:
|
|
logger.info(err)
|
|
raise Exception(err)
|
|
except urllib.error.URLError as e:
|
|
raise Exception("bettercap unreachable: %s" % e.reason)
|
|
|
|
def session(self):
|
|
"""GET /api/session — current bettercap state."""
|
|
return self._request('GET', '/session')
|
|
|
|
def run(self, command, verbose_errors=True):
|
|
"""POST /api/session — execute a bettercap command."""
|
|
return self._request('POST', '/session', {'cmd': command},
|
|
verbose_errors=verbose_errors)
|
|
|
|
def events(self):
|
|
"""GET /api/events — poll recent events (REST fallback)."""
|
|
try:
|
|
result = self._request('GET', '/events', verbose_errors=False)
|
|
# Clear after reading so we don't reprocess
|
|
try:
|
|
self.run('events.clear', verbose_errors=False)
|
|
except Exception:
|
|
pass
|
|
return result if isinstance(result, list) else []
|
|
except Exception:
|
|
return []
|
|
|
|
async def start_websocket(self, consumer, stop_event=None):
|
|
"""Connect to bettercap websocket event stream.
|
|
|
|
Args:
|
|
consumer: async callable that receives each message string.
|
|
stop_event: optional threading.Event — exit when set.
|
|
"""
|
|
import websockets
|
|
import asyncio
|
|
ws_url = "%s/events" % self.websocket
|
|
while not (stop_event and stop_event.is_set()):
|
|
try:
|
|
async with websockets.connect(ws_url, ping_interval=60,
|
|
ping_timeout=90) as ws:
|
|
async for msg in ws:
|
|
if stop_event and stop_event.is_set():
|
|
return
|
|
try:
|
|
await consumer(msg)
|
|
except Exception as ex:
|
|
logger.debug("Error parsing event: %s", ex)
|
|
except Exception as ex:
|
|
if stop_event and stop_event.is_set():
|
|
return
|
|
logger.debug("Websocket error: %s — reconnecting...", ex)
|
|
await asyncio.sleep(2)
|