mirror of
https://github.com/ziirish/burp-ui.git
synced 2026-05-15 06:05:58 -06:00
1122 lines
38 KiB
Python
1122 lines
38 KiB
Python
# -*- coding: utf8 -*-
|
|
"""
|
|
.. module:: burpui.misc.backend.parallel
|
|
:platform: Unix
|
|
:synopsis: Burp-UI parallel backend module.
|
|
|
|
.. moduleauthor:: Ziirish <hi+burpui@ziirish.me>
|
|
|
|
"""
|
|
import json
|
|
import os
|
|
import re
|
|
import ssl
|
|
import struct
|
|
import time
|
|
from asyncio import iscoroutinefunction
|
|
from functools import partial
|
|
|
|
import trio
|
|
|
|
from ..._compat import to_bytes, to_unicode
|
|
from ...decorators import implement, usetriorun
|
|
from ...exceptions import BUIserverException
|
|
from ...tools.logging import logger
|
|
from ...utils import utc_to_local
|
|
from ..parser.burp2 import Parser
|
|
from .burp2 import Burp as Burp2
|
|
from .interface import BUIBACKEND_INTERFACE_METHODS, BUIbackend
|
|
from .utils.constant import BURP_STATUS_FORMAT_V2
|
|
|
|
BUI_DEFAULTS = {
|
|
"Parallel": {
|
|
"host": "::1",
|
|
"port": 11111,
|
|
"ssl": True,
|
|
"password": "password123456",
|
|
"timeout": 15,
|
|
"concurrency": 2,
|
|
"init_wait": 15,
|
|
},
|
|
}
|
|
|
|
|
|
class Connector:
|
|
logger = logger
|
|
|
|
def __init__(self, conf):
|
|
"""Connector client
|
|
|
|
:param conf: Configuration to use
|
|
:type conf: :class:`burpui.config.BUIConfig`
|
|
"""
|
|
self.host = conf.safe_get("host", section="Parallel", defaults=BUI_DEFAULTS)
|
|
self.port = conf.safe_get(
|
|
"port", "integer", section="Parallel", defaults=BUI_DEFAULTS
|
|
)
|
|
self.ssl = conf.safe_get(
|
|
"ssl", "boolean", section="Parallel", defaults=BUI_DEFAULTS
|
|
)
|
|
self.password = conf.safe_get(
|
|
"password", section="Parallel", defaults=BUI_DEFAULTS
|
|
)
|
|
self.timeout = conf.safe_get(
|
|
"timeout", "integer", section="Parallel", defaults=BUI_DEFAULTS
|
|
)
|
|
|
|
self.logger.debug(f"Monitor {self.host}:{self.port} - ssl: {self.ssl}")
|
|
|
|
self.connected = False
|
|
|
|
async def conn(self):
|
|
try:
|
|
if self.ssl:
|
|
ctx = ssl.SSLContext()
|
|
ctx.verify_mode = ssl.CERT_NONE
|
|
ctx.check_hostname = False
|
|
ctx.options |= (
|
|
ssl.OP_NO_SSLv2
|
|
| ssl.OP_NO_SSLv3
|
|
| ssl.OP_NO_TLSv1
|
|
| ssl.OP_NO_TLSv1_1
|
|
) # RFC 7540 Section 9.2: MUST be TLS >=1.2
|
|
ctx.options |= (
|
|
ssl.OP_NO_COMPRESSION
|
|
) # RFC 7540 Section 9.2.1: MUST disable compression
|
|
ctx.load_default_certs()
|
|
self.client_stream = await trio.open_ssl_over_tcp_stream(
|
|
self.host, self.port, ssl_context=ctx
|
|
)
|
|
else:
|
|
self.client_stream = await trio.open_tcp_stream(self.host, self.port)
|
|
except OSError as exc:
|
|
raise BUIserverException(str(exc))
|
|
|
|
self.logger.debug("Connected")
|
|
self.connected = True
|
|
return self.client_stream
|
|
|
|
async def _send(self, data):
|
|
data = to_bytes(data)
|
|
length = struct.pack("!Q", len(data))
|
|
await self.client_stream.send_all(length)
|
|
self.logger.debug(f"Sending: {data!r}")
|
|
await self.client_stream.send_all(data)
|
|
|
|
async def _do_process(self, data):
|
|
res = "[]"
|
|
await self._send(data)
|
|
tmp = await self.client_stream.receive_some(2)
|
|
tmp = to_unicode(tmp)
|
|
if tmp == "ER":
|
|
lengthbuf = await self.client_stream.receive_some(8)
|
|
(length,) = struct.unpack("!Q", lengthbuf)
|
|
err = await self.receive_all(length)
|
|
err = to_unicode(err)
|
|
raise BUIserverException(err)
|
|
if tmp != "OK":
|
|
self.logger.debug("Ooops, unsuccessful!")
|
|
return res
|
|
self.logger.debug("Data sent successfully")
|
|
lengthbuf = await self.client_stream.receive_some(8)
|
|
(length,) = struct.unpack("!Q", lengthbuf)
|
|
res = await self.receive_all(length)
|
|
res = to_unicode(res)
|
|
self.logger.debug(f"Received: {res!r}")
|
|
return res
|
|
|
|
async def _process(self, data):
|
|
if not self.connected:
|
|
await self.conn()
|
|
async with self.client_stream:
|
|
result = await self._do_process(data)
|
|
self.connected = False
|
|
else:
|
|
result = await self._do_process(data)
|
|
return result
|
|
|
|
async def status(self, query, timeout=None, cache=True):
|
|
request = {
|
|
"query": query,
|
|
"timeout": timeout,
|
|
"cache": cache,
|
|
"password": self.password,
|
|
}
|
|
request = json.dumps(request)
|
|
result = await self._process(request)
|
|
return json.loads(result)
|
|
|
|
async def request(self, func, *args, **kwargs):
|
|
req = {
|
|
"func": func,
|
|
"args": args,
|
|
"kwargs": kwargs,
|
|
"password": self.password,
|
|
}
|
|
req = json.dumps(req)
|
|
result = await self._process(req)
|
|
return result
|
|
|
|
async def receive_all(self, length=1024, bsize=None):
|
|
buf = b""
|
|
bsize = bsize if bsize is not None else 1024
|
|
bsize = min(bsize, length)
|
|
received = 0
|
|
tries = 0
|
|
while received < length:
|
|
newbuf = await self.client_stream.receive_some(bsize)
|
|
if not newbuf:
|
|
# 3 successive read failure => raise exception
|
|
if tries > 3:
|
|
raise IOError("Unable to read full response")
|
|
tries += 1
|
|
trio.sleep(0.1)
|
|
continue
|
|
# reset counter
|
|
tries = 0
|
|
buf += newbuf
|
|
received += len(newbuf)
|
|
return buf
|
|
|
|
|
|
# Asynchronous functions for the bui-monitor pool
|
|
class AsyncBurpMixin:
|
|
async def _async_statistics(self, agent=None):
|
|
return json.loads(await self._async_request("statistics"))
|
|
|
|
async def _async_get_client_version(self, agent=None):
|
|
if self._client_version is None:
|
|
try:
|
|
self._client_version = await self._async_request("client_version")
|
|
except BUIserverException:
|
|
return ""
|
|
return self._client_version or ""
|
|
|
|
async def _async_get_server_version(self, agent=None):
|
|
if self._server_version is None:
|
|
try:
|
|
self._server_version = await self._async_request("server_version")
|
|
except BUIserverException:
|
|
return ""
|
|
return self._server_version or ""
|
|
|
|
async def _async_status(self, query="c:\n", timeout=None, cache=True, agent=None):
|
|
try:
|
|
connector = Connector(self.conf)
|
|
return await connector.status(query, timeout, cache)
|
|
except (OSError, IOError) as exc:
|
|
raise BUIserverException(str(exc))
|
|
if not self._ready:
|
|
self.init_all()
|
|
|
|
async def _async_request(self, func, *args, **kwargs):
|
|
try:
|
|
connector = Connector(self.conf)
|
|
return await connector.request(func, *args, **kwargs)
|
|
except (OSError, IOError) as exc:
|
|
raise BUIserverException(str(exc))
|
|
|
|
async def _async_get_backup_logs(
|
|
self, number, client, forward=False, deep=False, store=None, limit=None
|
|
):
|
|
async def _do_stuff():
|
|
nonlocal client
|
|
nonlocal number
|
|
nonlocal forward
|
|
nonlocal deep
|
|
bucket1 = []
|
|
bucket2 = []
|
|
ret = {}
|
|
query = await self._async_status("c:{0}:b:{1}\n".format(client, number))
|
|
if not query:
|
|
return ret
|
|
try:
|
|
logs = query["clients"][0]["backups"][0]["logs"]["list"]
|
|
except KeyError:
|
|
self.logger.warning("No logs found")
|
|
return ret
|
|
async with trio.open_nursery() as nursery:
|
|
if "backup_stats" in logs:
|
|
nursery.start_soon(
|
|
self._async_parse_backup_stats,
|
|
number,
|
|
client,
|
|
forward,
|
|
None,
|
|
bucket1,
|
|
)
|
|
if "backup" in logs and deep:
|
|
nursery.start_soon(
|
|
self._async_parse_backup_log, number, client, bucket2
|
|
)
|
|
|
|
if bucket1:
|
|
ret = bucket1[0]
|
|
if bucket2:
|
|
ret.update(bucket2[0])
|
|
|
|
ret["encrypted"] = False
|
|
if "files_enc" in ret and ret["files_enc"]["total"] > 0:
|
|
ret["encrypted"] = True
|
|
return ret
|
|
|
|
if limit is not None:
|
|
async with limit:
|
|
res = await _do_stuff()
|
|
else:
|
|
res = await _do_stuff()
|
|
|
|
if store is not None:
|
|
store.append(res)
|
|
else:
|
|
return res
|
|
|
|
async def _async_get_all_backup_logs(self, client, forward=False, deep=False):
|
|
ret = []
|
|
backups = await self._async_get_client(client)
|
|
queue = []
|
|
limit = trio.CapacityLimiter(self.concurrency)
|
|
async with trio.open_nursery() as nursery:
|
|
for back in backups:
|
|
nursery.start_soon(
|
|
self._async_get_backup_logs,
|
|
back["number"],
|
|
client,
|
|
forward,
|
|
deep,
|
|
queue,
|
|
limit,
|
|
)
|
|
|
|
ret = sorted(queue, key=lambda x: x["number"])
|
|
return ret
|
|
|
|
async def _async_parse_backup_log(self, number, client, bucket=None):
|
|
query = await self._async_status(
|
|
"c:{0}:b:{1}:l:backup\n".format(client, number)
|
|
)
|
|
res = self._do_parse_backup_log(query, client)
|
|
if bucket is not None:
|
|
bucket.append(res)
|
|
return res
|
|
|
|
async def _async_parse_backup_stats(
|
|
self, number, client, forward=False, agent=None, bucket=None
|
|
):
|
|
"""The :func:`burpui.misc.backend.parallel.Burp._async_parse_backup_stats`
|
|
function is used to parse the burp logs.
|
|
|
|
:param number: Backup number to work on
|
|
:type number: int
|
|
|
|
:param client: Client name to work on
|
|
:type client: str
|
|
|
|
:param forward: Is the client name needed in later process
|
|
:type forward: bool
|
|
|
|
:param agent: What server to ask (only in multi-agent mode)
|
|
:type agent: str
|
|
|
|
:returns: Dict containing the backup log
|
|
"""
|
|
backup = {"os": await self._async_guess_os(client), "number": int(number)}
|
|
if forward:
|
|
backup["name"] = client
|
|
query = await self._async_status(
|
|
"c:{0}:b:{1}:l:backup_stats\n".format(client, number)
|
|
)
|
|
ret = self._do_parse_backup_stats(query, backup, number, client, forward, agent)
|
|
if bucket is not None:
|
|
bucket.append(ret)
|
|
return ret
|
|
|
|
async def _async_get_clients_report(self, clients, agent=None):
|
|
"""See :func:`burpui.misc.backend.interface.BUIbackend.get_clients_report`"""
|
|
|
|
async def __compute_client_report(cli, queue, limit):
|
|
async with limit:
|
|
if not cli:
|
|
return
|
|
client = await self._async_get_client(cli["name"])
|
|
if not client or not client[-1]:
|
|
return
|
|
stats = await self._async_get_backup_logs(
|
|
client[-1]["number"], cli["name"]
|
|
)
|
|
queue.append((cli, client, stats))
|
|
|
|
data = []
|
|
limiter = trio.CapacityLimiter(self.concurrency)
|
|
|
|
async with trio.open_nursery() as nursery:
|
|
for client in clients:
|
|
nursery.start_soon(__compute_client_report, client, data, limiter)
|
|
|
|
return self._do_get_clients_report(data)
|
|
|
|
async def _async_get_counters(self, name=None, agent=None):
|
|
ret = {}
|
|
query = await self._async_status("c:{0}\n".format(name), cache=False)
|
|
# check the status returned something
|
|
if not query:
|
|
return ret
|
|
|
|
try:
|
|
client = query["clients"][0]
|
|
except KeyError:
|
|
self.logger.warning("Client not found")
|
|
return ret
|
|
return self._do_get_counters(client)
|
|
|
|
async def _async_is_backup_running(self, name=None, agent=None):
|
|
"""See
|
|
:func:`burpui.misc.backend.interface.BUIbackend.is_backup_running`
|
|
"""
|
|
if not name:
|
|
return False
|
|
try:
|
|
query = await self._async_status("c:{0}\n".format(name))
|
|
except BUIserverException:
|
|
return False
|
|
return self._do_is_backup_running(query)
|
|
|
|
async def _async_is_one_backup_running(self, agent=None):
|
|
"""See
|
|
:func:`burpui.misc.backend.interface.BUIbackend.is_one_backup_running`
|
|
"""
|
|
ret = []
|
|
try:
|
|
clients = await self._async_get_all_clients(deep=False, last_attempt=False)
|
|
except BUIserverException:
|
|
return ret
|
|
return self._do_is_one_backup_running(clients)
|
|
|
|
async def _async_get_last_backup(self, name, working=True):
|
|
"""Return the last backup of a given client
|
|
|
|
:param name: Name of the client
|
|
:type name: str
|
|
|
|
:param working: Also return uncomplete backups
|
|
:type working: bool
|
|
|
|
:returns: The last backup
|
|
"""
|
|
try:
|
|
clients = await self._async_status("c:{}".format(name))
|
|
client = clients["clients"][0]
|
|
i = 0
|
|
while True:
|
|
ret = client["backups"][i]
|
|
if not working and "working" in ret["flags"]:
|
|
i += 1
|
|
continue
|
|
return ret
|
|
except (KeyError, IndexError, BUIserverException):
|
|
return None
|
|
|
|
async def _async_guess_os(self, name):
|
|
"""Return the OS of the given client based on the magic *os* label
|
|
|
|
:param name: Name of the client
|
|
:type name: str
|
|
|
|
:returns: The guessed OS of the client
|
|
|
|
::
|
|
|
|
grep label /etc/burp/clientconfdir/toto
|
|
label = os: Darwin OS
|
|
"""
|
|
ret = "Unknown"
|
|
if name in self._os_cache:
|
|
return self._os_cache[name]
|
|
|
|
labels = await self._async_get_client_labels(name)
|
|
OSES = []
|
|
|
|
for label in labels:
|
|
if re.match("os:", label, re.IGNORECASE):
|
|
_os = label.split(":", 1)[1].strip()
|
|
if _os not in OSES:
|
|
OSES.append(_os)
|
|
|
|
if OSES:
|
|
ret = OSES[-1]
|
|
else:
|
|
# more aggressive check
|
|
last = await self._async_get_last_backup(name, False)
|
|
if last:
|
|
try:
|
|
tree = await self._async_get_tree(name, last["number"])
|
|
|
|
if tree[0]["name"] != "/":
|
|
ret = "Windows"
|
|
else:
|
|
ret = "Unix/Linux"
|
|
except (IndexError, KeyError, BUIserverException):
|
|
pass
|
|
|
|
self._os_cache[name] = ret
|
|
return ret
|
|
|
|
async def _async_get_all_clients(self, agent=None, deep=True, last_attempt=True):
|
|
ret = []
|
|
query = await self._async_status()
|
|
if not query or "clients" not in query:
|
|
return ret
|
|
|
|
async def __compute_client_data(client, queue, limit):
|
|
async with limit:
|
|
cli = {}
|
|
cli["name"] = client["name"]
|
|
cli["state"] = self._status_human_readable(client["run_status"])
|
|
infos = client["backups"]
|
|
if cli["state"] in ["running"]:
|
|
cli["last"] = "now"
|
|
cli["last_attempt"] = "now"
|
|
elif not infos:
|
|
cli["last"] = "never"
|
|
cli["last_attempt"] = "never"
|
|
else:
|
|
convert = True
|
|
infos = infos[0]
|
|
server_version = await self._async_get_server_version()
|
|
if server_version and server_version < BURP_STATUS_FORMAT_V2:
|
|
cli["last"] = infos["timestamp"]
|
|
convert = False
|
|
# only do deep inspection when server >= BURP_STATUS_FORMAT_V2
|
|
if deep:
|
|
logs = await self._async_get_backup_logs(
|
|
infos["number"], client["name"]
|
|
)
|
|
cli["last"] = logs["start"]
|
|
else:
|
|
cli["last"] = utc_to_local(infos["timestamp"])
|
|
if last_attempt:
|
|
last_backup = await self._async_get_last_backup(client["name"])
|
|
if convert:
|
|
cli["last_attempt"] = utc_to_local(last_backup["timestamp"])
|
|
else:
|
|
cli["last_attempt"] = last_backup["timestamp"]
|
|
queue.append(cli)
|
|
|
|
clients = query["clients"]
|
|
limiter = trio.CapacityLimiter(self.concurrency)
|
|
|
|
async with trio.open_nursery() as nursery:
|
|
for client in clients:
|
|
nursery.start_soon(__compute_client_data, client, ret, limiter)
|
|
|
|
return ret
|
|
|
|
async def _async_get_client_status(self, name=None, agent=None):
|
|
ret = {}
|
|
if not name:
|
|
return ret
|
|
query = await self._async_status("c:{0}\n".format(name))
|
|
if not query:
|
|
return ret
|
|
try:
|
|
client = query["clients"][0]
|
|
except (KeyError, IndexError):
|
|
self.logger.warning("Client not found")
|
|
return ret
|
|
return self._do_get_client_status(client)
|
|
|
|
async def _async_get_client_filtered(
|
|
self, name=None, limit=-1, page=None, start=None, end=None, agent=None
|
|
):
|
|
ret = []
|
|
if not name:
|
|
return ret
|
|
query = await self._async_status("c:{0}\n".format(name))
|
|
if not query:
|
|
return ret
|
|
try:
|
|
backups = query["clients"][0]["backups"]
|
|
except (KeyError, IndexError):
|
|
self.logger.warning("Client not found")
|
|
return ret
|
|
|
|
async def __parse_log(backup, client, back, ret, limiter):
|
|
async with limiter:
|
|
append = True
|
|
log = await self._async_get_backup_logs(backup["number"], client)
|
|
try:
|
|
back["encrypted"] = log["encrypted"]
|
|
try:
|
|
back["received"] = log["received"]
|
|
except KeyError:
|
|
back["received"] = 0
|
|
try:
|
|
back["size"] = log["totsize"]
|
|
except KeyError:
|
|
back["size"] = 0
|
|
back["end"] = log["end"]
|
|
# override date since the timestamp is odd
|
|
back["date"] = log["start"]
|
|
except Exception:
|
|
self.logger.warning("Unable to parse logs")
|
|
append = False
|
|
|
|
if append:
|
|
ret.append(back)
|
|
|
|
queue = []
|
|
limiter = trio.CapacityLimiter(self.concurrency)
|
|
|
|
async with trio.open_nursery() as nursery:
|
|
for idx, backup in enumerate(backups):
|
|
back = {}
|
|
# skip the first elements if we are in a page
|
|
if page and page > 1 and limit > 0:
|
|
if idx < (page - 1) * limit:
|
|
continue
|
|
|
|
# skip running backups since data will be inconsistent
|
|
if "flags" in backup and "working" in backup["flags"]:
|
|
continue
|
|
back["number"] = backup["number"]
|
|
if "flags" in backup and "deletable" in backup["flags"]:
|
|
back["deletable"] = True
|
|
else:
|
|
back["deletable"] = False
|
|
back["date"] = backup["timestamp"]
|
|
# skip backups before "start"
|
|
if start and backup["timestamp"] < start:
|
|
continue
|
|
# skip backups after "end"
|
|
if end and backup["timestamp"] > end:
|
|
continue
|
|
|
|
nursery.start_soon(__parse_log, backup, name, back, queue, limiter)
|
|
|
|
# stop after "limit" elements
|
|
if page and page > 1 and limit > 0:
|
|
if idx >= page * limit:
|
|
break
|
|
elif limit > 0 and idx >= limit:
|
|
break
|
|
|
|
# Here we need to reverse the array so the backups are sorted by num
|
|
# ASC
|
|
ret = sorted(queue, key=lambda x: x["number"])
|
|
return ret
|
|
|
|
async def _async_get_tree(
|
|
self, name=None, backup=None, root=None, level=-1, agent=None
|
|
):
|
|
ret = []
|
|
if not name or not backup:
|
|
return ret
|
|
if not root:
|
|
top = ""
|
|
else:
|
|
top = to_unicode(root)
|
|
|
|
# we know this operation may take a while so we arbitrary increase the
|
|
# read timeout
|
|
timeout = None
|
|
if top == "*":
|
|
timeout = max(self.timeout, 300)
|
|
|
|
query = await self._async_status(
|
|
"c:{0}:b:{1}:p:{2}\n".format(name, backup, top), timeout
|
|
)
|
|
return self._format_tree(query, top, level)
|
|
|
|
async def _async_get_client(self, name=None, agent=None):
|
|
return await self._async_get_client_filtered(name)
|
|
|
|
async def _async_get_client_labels(self, client=None, agent=None):
|
|
"""See
|
|
:func:`burpui.misc.backend.interface.BUIbackend.get_client_labels`
|
|
"""
|
|
ret = []
|
|
if not client:
|
|
return ret
|
|
# micro optimization since the status results are cached in memory for a
|
|
# couple seconds, using the same global query and iterating over it
|
|
# will be more efficient than filtering burp-side
|
|
query = await self._async_status("c:\n")
|
|
if not query:
|
|
return ret
|
|
try:
|
|
for cli in query["clients"]:
|
|
if cli["name"] == client:
|
|
return cli["labels"]
|
|
except KeyError:
|
|
return ret
|
|
|
|
async def _async_restore_files(
|
|
self,
|
|
name=None,
|
|
backup=None,
|
|
files=None,
|
|
strip=None,
|
|
archive="zip",
|
|
password=None,
|
|
agent=None,
|
|
):
|
|
return await trio.to_thread.run_sync(
|
|
Burp2.restore_files,
|
|
self,
|
|
name,
|
|
backup,
|
|
files,
|
|
strip,
|
|
archive,
|
|
password,
|
|
agent,
|
|
)
|
|
|
|
async def _async_is_backup_deletable(self, name=None, backup=None, agent=None):
|
|
if not name or not backup:
|
|
return False
|
|
query = await self._async_status("c:{0}:b:{1}\n".format(name, backup))
|
|
if not query:
|
|
return False
|
|
return self._do_is_backup_deletable(query)
|
|
|
|
|
|
# Some functions are the same as in Burp1 backend
|
|
class Burp(Burp2, AsyncBurpMixin):
|
|
"""The :class:`burpui.misc.backend.parallel.Burp` class provides a consistent
|
|
backend for ``burp-2`` servers through the bui-monitor pool. It is also able to
|
|
perform some operations asynchronously to speedup the whole API.
|
|
|
|
It extends the :class:`burpui.misc.backend.burp2.Burp` class because a few
|
|
functions can be reused. The rest is just overridden.
|
|
|
|
:param server: ``Burp-UI`` server instance in order to access logger
|
|
and/or some global settings
|
|
:type server: :class:`burpui.engines.server.BUIServer`
|
|
|
|
:param conf: Configuration to use
|
|
:type conf: :class:`burpui.config.BUIConfig`
|
|
"""
|
|
|
|
# backend version
|
|
_vers = 2
|
|
# cache to store the guessed OS
|
|
_os_cache = {}
|
|
|
|
_client_version = None
|
|
_server_version = None
|
|
_batch_list_supported = None
|
|
|
|
_ready = False
|
|
parser = None
|
|
|
|
def __init__(self, server=None, conf=None):
|
|
"""
|
|
:param server: ``Burp-UI`` server instance in order to access logger
|
|
and/or some global settings
|
|
:type server: :class:`burpui.engines.server.BUIServer`
|
|
|
|
:param conf: Configuration to use
|
|
:type conf: :class:`burpui.config.BUIConfig`
|
|
"""
|
|
|
|
BUIbackend.__init__(self, server, conf)
|
|
|
|
self.conf = conf
|
|
self.concurrency = conf.safe_get(
|
|
"concurrency", "integer", "Parallel", BUI_DEFAULTS
|
|
)
|
|
self.init_wait = conf.safe_get("init_wait", "integer", "Parallel", BUI_DEFAULTS)
|
|
|
|
if os.getenv("BUI_MODE", "") == "celery":
|
|
# we cap the concurrency level in order not to prevent our main server
|
|
# to talk to the monitor
|
|
self.concurrency = max(1, self.concurrency // 2)
|
|
|
|
self.logger.info("burp conf cli: {}".format(self.burpconfcli))
|
|
self.logger.info("burp conf srv: {}".format(self.burpconfsrv))
|
|
self.logger.info("command timeout: {}".format(self.timeout))
|
|
self.logger.info("tmpdir: {}".format(self.tmpdir))
|
|
self.logger.info("zip64: {}".format(self.zip64))
|
|
self.logger.info("includes: {}".format(self.includes))
|
|
self.logger.info("enforce: {}".format(self.enforce))
|
|
self.logger.info("revoke: {}".format(self.revoke))
|
|
self.logger.info("concurrency: {}".format(self.concurrency))
|
|
self.logger.info("init_wait: {}".format(self.init_wait))
|
|
|
|
if self.init_wait:
|
|
exc = None
|
|
for i in range(self.init_wait):
|
|
connector = Connector(conf)
|
|
try:
|
|
self.logger.warning(
|
|
"monitor not ready, waiting for it... {}/{}".format(
|
|
i, self.init_wait
|
|
)
|
|
)
|
|
trio.run(connector.conn)
|
|
if connector.connected:
|
|
trio.run(connector._send, "RE")
|
|
break
|
|
except BUIserverException as eee:
|
|
exc = eee
|
|
time.sleep(1)
|
|
else:
|
|
self.logger.error(f"monitor not ready, giving up!: {exc}")
|
|
|
|
try:
|
|
stats = self.statistics()
|
|
if "alive" in stats and stats["alive"]:
|
|
self.init_all()
|
|
except BUIserverException:
|
|
pass
|
|
|
|
def init_all(self):
|
|
self._ready = True
|
|
self.parser = Parser(self)
|
|
|
|
@property
|
|
def client_version(self):
|
|
return self.get_client_version()
|
|
|
|
@property
|
|
def server_version(self):
|
|
return self.get_server_version()
|
|
|
|
@property
|
|
def batch_list_supported(self):
|
|
if self._batch_list_supported is None:
|
|
self._batch_list_supported = json.loads(
|
|
trio.run(self._async_request, "batch_list_supported")
|
|
)
|
|
return self._batch_list_supported
|
|
|
|
@usetriorun
|
|
def statistics(self, agent=None):
|
|
return trio.run(self._async_statistics)
|
|
|
|
@usetriorun
|
|
def get_client_version(self, agent=None):
|
|
if self._client_version is None:
|
|
self._client_version = trio.run(self._async_get_client_version)
|
|
return self._client_version or ""
|
|
|
|
@usetriorun
|
|
def get_server_version(self, agent=None):
|
|
if self._server_version is None:
|
|
self._server_version = trio.run(self._async_get_server_version)
|
|
return self._server_version or ""
|
|
|
|
@usetriorun
|
|
def status(self, query="c:\n", timeout=None, cache=True, agent=None):
|
|
"""See :func:`burpui.misc.backend.interface.BUIbackend.status`"""
|
|
return trio.run(self._async_status, query, timeout, cache)
|
|
|
|
def get_backup_logs(self, number, client, forward=False, deep=False, agent=None):
|
|
"""See
|
|
:func:`burpui.misc.backend.interface.BUIbackend.get_backup_logs`
|
|
"""
|
|
if not client or not number:
|
|
return {} if number and number != -1 else []
|
|
|
|
if number == -1:
|
|
return trio.run(self._async_get_all_backup_logs, client, forward, deep)
|
|
return trio.run(self._async_get_backup_logs, number, client, forward, deep)
|
|
|
|
def _parse_backup_log(self, number, client):
|
|
"""The :func:`burpui.misc.backend.burp2.Burp._parse_backup_log`
|
|
function helps you determine if the backup is protocol 2 or 1 and various
|
|
useful details.
|
|
|
|
:param number: Backup number to work on
|
|
:type number: int
|
|
|
|
:param client: Client name to work on
|
|
:type client: str
|
|
|
|
:returns: a dict with some useful details
|
|
"""
|
|
return trio.run(self._async_parse_backup_log, number, client)
|
|
|
|
@usetriorun
|
|
def get_clients_report(self, clients, agent=None):
|
|
"""See :func:`burpui.misc.backend.interface.BUIbackend.get_clients_report`"""
|
|
return trio.run(self._async_get_clients_report, clients)
|
|
|
|
@usetriorun
|
|
def get_counters(self, name=None, agent=None):
|
|
"""See :func:`burpui.misc.backend.interface.BUIbackend.get_counters`"""
|
|
return trio.run(self._async_get_counters, name)
|
|
|
|
@usetriorun
|
|
def is_backup_running(self, name=None, agent=None):
|
|
"""See
|
|
:func:`burpui.misc.backend.interface.BUIbackend.is_backup_running`
|
|
"""
|
|
return trio.run(self._async_is_backup_running, name)
|
|
|
|
@usetriorun
|
|
def is_one_backup_running(self, agent=None):
|
|
"""See
|
|
:func:`burpui.misc.backend.interface.BUIbackend.is_one_backup_running`
|
|
"""
|
|
return trio.run(self._async_is_one_backup_running)
|
|
|
|
@usetriorun
|
|
def _get_last_backup(self, name, working=True):
|
|
"""Return the last backup of a given client
|
|
|
|
:param name: Name of the client
|
|
:type name: str
|
|
|
|
:param working: Also return uncomplete backups
|
|
:type working: bool
|
|
|
|
:returns: The last backup
|
|
"""
|
|
return trio.run(self._async_get_last_backup, name, working)
|
|
|
|
@usetriorun
|
|
def _guess_os(self, name):
|
|
"""Return the OS of the given client based on the magic *os* label
|
|
|
|
:param name: Name of the client
|
|
:type name: str
|
|
|
|
:returns: The guessed OS of the client
|
|
|
|
::
|
|
|
|
grep label /etc/burp/clientconfdir/toto
|
|
label = os: Darwin OS
|
|
"""
|
|
return trio.run(self._async_guess_os, name)
|
|
|
|
def get_all_clients(self, agent=None, last_attempt=True):
|
|
"""See
|
|
:func:`burpui.misc.backend.interface.BUIbackend.get_all_clients`
|
|
"""
|
|
# don't need async processing if burp-server < BURP_STATUS_FORMAT_V2
|
|
if not self.deep_inspection or (
|
|
self.server_version and self.server_version < BURP_STATUS_FORMAT_V2
|
|
):
|
|
return Burp2.get_all_clients(self, last_attempt=last_attempt)
|
|
# the deep inspection can take advantage of async processing
|
|
callback = partial(self._async_get_all_clients, last_attempt=last_attempt)
|
|
return trio.run(callback)
|
|
|
|
@usetriorun
|
|
def get_client_status(self, name=None, agent=None):
|
|
"""See
|
|
:func:`burpui.misc.backend.interface.BUIbackend.get_client_status`
|
|
"""
|
|
return trio.run(self._async_get_client_status, name)
|
|
|
|
@usetriorun
|
|
def get_client(self, name=None, agent=None):
|
|
"""See :func:`burpui.misc.backend.interface.BUIbackend.get_client`"""
|
|
return trio.run(self._async_get_client, name)
|
|
|
|
@usetriorun
|
|
def get_client_filtered(
|
|
self, name=None, limit=-1, page=None, start=None, end=None, agent=None
|
|
):
|
|
"""See :func:`burpui.misc.backend.interface.BUIbackend.get_client_filtered`"""
|
|
return trio.run(self._async_get_client_filtered, name, limit, page, start, end)
|
|
|
|
@usetriorun
|
|
def is_backup_deletable(self, name=None, backup=None, agent=None):
|
|
"""See
|
|
:func:`burpui.misc.backend.interface.BUIbackend.is_backup_deletable`
|
|
"""
|
|
return trio.run(self._async_is_backup_deletable, name, backup)
|
|
|
|
@usetriorun
|
|
def get_tree(self, name=None, backup=None, root=None, level=-1, agent=None):
|
|
"""See :func:`burpui.misc.backend.interface.BUIbackend.get_tree`"""
|
|
return trio.run(self._async_get_tree, name, backup, root, level)
|
|
|
|
@usetriorun
|
|
def get_client_labels(self, client=None, agent=None):
|
|
"""See
|
|
:func:`burpui.misc.backend.interface.BUIbackend.get_client_labels`
|
|
"""
|
|
return trio.run(self._async_get_client_labels, client)
|
|
|
|
@usetriorun
|
|
def restore_files(
|
|
self,
|
|
name=None,
|
|
backup=None,
|
|
files=None,
|
|
strip=None,
|
|
archive="zip",
|
|
password=None,
|
|
agent=None,
|
|
):
|
|
return trio.run(
|
|
self._async_restore_files,
|
|
name,
|
|
backup,
|
|
files,
|
|
strip,
|
|
archive,
|
|
password,
|
|
agent,
|
|
)
|
|
|
|
# Same as in Burp1 backend
|
|
|
|
# def read_conf_cli(self, agent=None):
|
|
|
|
# def read_conf_srv(self, agent=None):
|
|
|
|
# def store_conf_cli(self, data, agent=None):
|
|
|
|
# def store_conf_srv(self, data, agent=None):
|
|
|
|
# def get_parser_attr(self, attr=None, agent=None):
|
|
|
|
|
|
# Make every "Burp" method async
|
|
class AsyncBurp(Burp):
|
|
@property
|
|
async def batch_list_supported(self):
|
|
if self._batch_list_supported is None:
|
|
self._batch_list_supported = json.loads(
|
|
await self._async_request("batch_list_supported")
|
|
)
|
|
return self._batch_list_supported
|
|
|
|
# this method must not be async!
|
|
@implement
|
|
def statistics(self, agent=None):
|
|
return Burp.statistics(self)
|
|
|
|
# this method must not be async!
|
|
@implement
|
|
def get_parser(self, agent=None):
|
|
"""See :func:`burpui.misc.backend.interface.BUIbackend.get_parser`"""
|
|
return Burp.get_parser(self)
|
|
|
|
@property
|
|
def client_version(self):
|
|
return trio.run(self.get_client_version)
|
|
|
|
@property
|
|
def server_version(self):
|
|
return trio.run(self.get_server_version)
|
|
|
|
@implement
|
|
async def get_client_version(self, agent=None):
|
|
if self._client_version is None:
|
|
self._client_version = await self._async_request("client_version")
|
|
return self._client_version
|
|
|
|
@implement
|
|
async def get_server_version(self, agent=None):
|
|
if self._server_version is None:
|
|
self._server_version = await self._async_request("server_version")
|
|
return self._server_version
|
|
|
|
@implement
|
|
async def get_backup_logs(
|
|
self, number, client, forward=False, deep=False, agent=None
|
|
):
|
|
"""See
|
|
:func:`burpui.misc.backend.interface.BUIbackend.get_backup_logs`
|
|
"""
|
|
if not client or not number:
|
|
return {} if number and number != -1 else []
|
|
|
|
if number == -1:
|
|
return await self._async_get_all_backup_logs(client, forward, deep)
|
|
return await self._async_get_backup_logs(number, client, forward, deep)
|
|
|
|
@implement
|
|
async def get_all_clients(self, agent=None, last_attempt=True):
|
|
"""See
|
|
:func:`burpui.misc.backend.interface.BUIbackend.get_all_clients`
|
|
"""
|
|
return await self._async_get_all_clients(last_attempt=last_attempt)
|
|
|
|
@implement
|
|
async def get_attr(self, name, default=None, agent=None):
|
|
"""See :func:`burpui.misc.backend.interface.BUIbackend.get_attr`"""
|
|
try:
|
|
try:
|
|
return await getattr(self, name, default)
|
|
except TypeError:
|
|
return getattr(self, name, default)
|
|
except AttributeError:
|
|
return default
|
|
|
|
def __getattribute__(self, name):
|
|
if name in BUIBACKEND_INTERFACE_METHODS or name in ["_guess_os"]:
|
|
wrap = False
|
|
proxy = True
|
|
func = None
|
|
try:
|
|
func = object.__getattribute__(self, name)
|
|
proxy = not getattr(func, "__ismethodimplemented__", False)
|
|
wrap = getattr(func, "__isusingtriorun__", False)
|
|
except:
|
|
pass
|
|
if func and not callable(func):
|
|
self.logger.debug(f"{func} is not a function")
|
|
return func
|
|
self.logger.debug(f"async func: {func} - proxy: {proxy}, wrap: {wrap}")
|
|
if wrap:
|
|
realname = (
|
|
f"_async_{name}" if not name.startswith("_") else f"_async{name}"
|
|
)
|
|
realfunc = object.__getattribute__(self, realname)
|
|
return ProxyAsyncCall(realfunc, realname, self)
|
|
if proxy:
|
|
return ProxyAsyncCall(func, name, self)
|
|
elif func:
|
|
return func
|
|
return object.__getattribute__(self, name)
|
|
|
|
|
|
class ProxyAsyncCall(object):
|
|
"""Class to dispatch call of unknown methods in order to dynamically
|
|
implements their async variant."""
|
|
|
|
def __init__(self, func, name, proxy):
|
|
"""
|
|
:param proxy: function to proxify
|
|
:type proxy: function
|
|
|
|
:param name: Name of the method to proxify
|
|
:type name: str
|
|
|
|
:param proxy: Object to proxify
|
|
:type proxy: :class:`Burp`
|
|
"""
|
|
self.func = func
|
|
self.name = name
|
|
self.proxy = proxy
|
|
|
|
async def __call__(self, *args, **kwargs):
|
|
"""This is where the proxy call (and the magic) occurs"""
|
|
# retrieve the original function prototype
|
|
proto = getattr(BUIbackend, self.name, None) or getattr(self.proxy, self.name)
|
|
args_name = list(proto.__code__.co_varnames)
|
|
# skip self
|
|
args_name.pop(0)
|
|
# we transform unnamed arguments to named ones
|
|
# example:
|
|
# def my_function(toto, tata=None, titi=None):
|
|
#
|
|
# x = my_function('blah', titi='blih')
|
|
#
|
|
# => {'toto': 'blah', 'titi': 'blih'}
|
|
encoded_args = {}
|
|
for idx, opt in enumerate(args):
|
|
encoded_args[args_name[idx]] = opt
|
|
encoded_args.update(kwargs)
|
|
|
|
if iscoroutinefunction(self.func):
|
|
return await self.func(**encoded_args)
|
|
return self.func(**encoded_args)
|