use chrono::{DateTime, Utc};
use serde_json::{Value, json};
use super::collect::{self, RouteSource};
use super::effects::{self, Ctx, Notifier, Outcome};
use super::request::{self, BridgeRequest, Op};
use super::wire::{self, GuardEvent};
use super::{ProError, ProResult, ProjectScope, watch};
pub(crate) const MAX_FRAME_BYTES: usize = 1 << 20;
pub struct Env<'a> {
pub scope: &'a ProjectScope,
pub routes: &'a dyn RouteSource,
pub notifier: &'a dyn Notifier,
pub now: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Reply {
pub status: u16,
pub body: Value,
}
impl Reply {
fn ok(body: Value) -> Self {
Self { status: 200, body }
}
fn error(error: &ProError) -> Self {
Self {
status: error.status,
body: super::error_body(error),
}
}
}
pub fn handle_inputs(env: &Env<'_>, raw: &str) -> Reply {
let request = match request::parse(raw).and_then(|r| scoped(env, r)) {
Ok(request) => request,
Err(error) => return Reply::error(&error),
};
let (guard_new, guard_cursor) = if request.op == Op::AlertsEvaluate {
match watch::pending(env.scope) {
Ok(Some((events, cursor))) => (Some(events), Some(cursor)),
Ok(None) => return Reply::ok(json!({ "skip": true })),
Err(error) => return Reply::error(&error),
}
} else {
(None, None)
};
match prepare(env, &request, guard_new) {
Ok(prepared) => {
let mut body = json!({ "request": prepared });
if let Some(cursor) = guard_cursor {
body["guard_cursor"] = json!(cursor);
}
Reply::ok(body)
}
Err(error) => Reply::error(&error),
}
}
pub fn handle_apply(env: &Env<'_>, raw: &str) -> Reply {
if raw.len() > MAX_FRAME_BYTES + request::MAX_REQUEST_BYTES {
return Reply::error(&ProError::new(413, "Request too large"));
}
let Ok(Value::Object(mut object)) = serde_json::from_str::<Value>(raw) else {
return Reply::error(&ProError::bad("Invalid JSON request"));
};
let response = object.remove("response");
let guard_cursor = object.remove("guard_cursor");
let request = match request::from_object(object).and_then(|r| scoped(env, r)) {
Ok(request) => request,
Err(error) => return Reply::error(&error),
};
let Some(Ok(response)) = response.map(serde_json::from_value::<wire::Response>) else {
return Reply::error(&ProError::bad(
"The analysis answer is missing or unreadable",
));
};
let cursor = match (request.op, guard_cursor.as_ref().map(Value::as_u64)) {
(Op::AlertsEvaluate, Some(Some(cursor))) => Some(cursor),
(Op::AlertsEvaluate, _) => {
return Reply::error(&ProError::bad("alerts.evaluate needs its guard_cursor"));
}
(_, Some(_)) => return Reply::error(&ProError::bad("Unknown request field")),
(_, None) => None,
};
let mut body = match finish(env, &request, response) {
Ok(body) => body,
Err(error) => return Reply::error(&error),
};
if let Some(cursor) = cursor
&& let Err(error) = watch::advance(env.scope, cursor)
{
return Reply::error(&error);
}
body["status"] = json!(200);
Reply::ok(body)
}
fn scoped(env: &Env<'_>, request: BridgeRequest) -> ProResult<BridgeRequest> {
if env.scope.is_device() && !request.op.device_wide() {
return Err(ProError::bad(
"Choose a project: this view reads one project's data",
));
}
Ok(request)
}
pub(crate) fn prepare(
env: &Env<'_>,
request: &BridgeRequest,
guard_new: Option<Vec<GuardEvent>>,
) -> ProResult<wire::Request> {
let collected = collect::inputs(request, env.scope, env.routes, env.now, guard_new)?;
let mut payload = wire::Request {
schema: wire::SCHEMA.to_string(),
op: request.op.name().to_string(),
now_utc: env.now.to_rfc3339(),
project: env.scope.project(),
query: request.query.clone(),
body: Value::Object(request.body.clone()),
inputs: collected.inputs,
};
let size = match request.op {
Op::MemoryAnalyze | Op::MemorySnapshot => {
fit_newest(&mut payload, "facts", MAX_FRAME_BYTES)?
}
_ => encoded_len(&payload)?,
};
if size > MAX_FRAME_BYTES {
return Err(ProError::new(
413,
"This view needs more data than one analysis request can carry; narrow the period or filters, or consolidate memory first",
));
}
Ok(payload)
}
fn encoded_len(payload: &wire::Request) -> ProResult<usize> {
serde_json::to_vec(payload)
.map(|bytes| bytes.len())
.map_err(|_| ProError::internal("The request could not be encoded"))
}
pub(crate) fn fit_newest(payload: &mut wire::Request, key: &str, max: usize) -> ProResult<usize> {
let mut size = encoded_len(payload)?;
while size > max {
let Some(Value::Array(items)) = payload.inputs.get_mut(key) else {
break;
};
if items.is_empty() {
break;
}
let drop = items.len().div_ceil(10);
items.drain(..drop);
size = encoded_len(payload)?;
}
Ok(size)
}
pub(crate) fn finish(
env: &Env<'_>,
request: &BridgeRequest,
response: wire::Response,
) -> ProResult<Value> {
match response.status {
200 => {}
400 | 404 | 409 => {
let message = response
.error
.map(|e| e.message)
.filter(|m| super::plain_text(m, 500))
.unwrap_or_else(|| "The request was refused".into());
return Err(ProError::new(response.status, message));
}
_ => return Err(ProError::new(502, "The analysis answer is unreadable")),
}
if let Some(plans) = response.plans.filter(|p| !p.is_empty()) {
let plans = effects::parse_plans(request.op, plans)?;
let ctx = Ctx {
scope: env.scope,
request,
now: env.now,
notifier: env.notifier,
};
let mut applied = Vec::new();
for outcome in effects::apply(&ctx, plans)? {
match outcome {
Outcome::Applied(readback) => applied.push(readback),
Outcome::File(file) => return Ok(json!({ "file": file })),
}
}
return Ok(json!({ "applied": applied, "body": response.body }));
}
if request.op.is_write() {
return Ok(json!({ "body": response.body }));
}
if let Some(file) = response.file {
if !super::plain_text(&file.name, 120)
|| !super::plain_text(&file.mime, 100)
|| file.name.contains(['/', '\\'])
{
return Err(ProError::new(502, "The analysis file is refused"));
}
return Ok(json!({ "file": file }));
}
Ok(json!({ "body": response.body }))
}
pub fn status_line(status: u16) -> &'static str {
match status {
200 => "200 OK",
400 => "400 Bad Request",
404 => "404 Not Found",
405 => "405 Method Not Allowed",
409 => "409 Conflict",
413 => "413 Payload Too Large",
502 => "502 Bad Gateway",
_ => "500 Internal Server Error",
}
}