use crate::px_debug;
use crate::pxconfig::PXConfig;
use crate::pxcontext::PXContext;
use crate::{handlers::pxcrypto, modules::pxconstants::*};
use base64::{Engine as _, engine::general_purpose};
use fastly::{Error, Request, Response};
use serde_json::json;
use std::time::{SystemTime, UNIX_EPOCH};
pub(crate) enum TelemetryUpdateReason {
Command,
RiskApi,
}
impl TelemetryUpdateReason {
fn as_str(&self) -> &str {
match self {
TelemetryUpdateReason::Command => "command",
TelemetryUpdateReason::RiskApi => "risk",
}
}
}
pub(crate) fn send_telemetry_activity(
conf: &PXConfig,
ctx: &PXContext,
update_reason: TelemetryUpdateReason,
) -> Result<Response, Error> {
let current_timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64;
let configs = json!({
"active_config": conf.build_config_json(),
"static_config": conf.build_static_json(),
"remote_config": json!({})
});
let payload = json!({
"type": "enforcer_telemetry",
"px_app_id": &conf.app_id,
"timestamp": current_timestamp,
"details": {
"enforcer_configs": configs,
"module_version": PX_MODULE_VERSION,
"update_reason": update_reason.as_str(),
"node_name": fastly::compute_runtime::hostname(),
"request_id": ctx.request_id.to_string()
}
});
px_debug!("Sending telemetry payload: {}", payload.to_string());
let url = format!("https://{}{}", conf.human_sapi_host, TELEMETRY_PATH);
let req = Request::post(url)
.with_header("Authorization", format!("Bearer {}", conf.auth_token))
.with_header("Content-Type", APPLICATION_JSON)
.with_body(payload.to_string());
Ok(req.send(&conf.human_sapi_backend)?)
}
pub fn handle_telemetry(req: &Request, conf: &PXConfig, ctx: &PXContext) {
px_debug!("Received command to send enforcer telemetry");
let header_value: String = req
.get_header_str_lossy(TELEMETRY_HEADER)
.map(|v| v.into_owned())
.unwrap_or_default();
let decoded_header_base64 = general_purpose::STANDARD
.decode(header_value)
.unwrap_or_default();
let decoded_header = std::str::from_utf8(&decoded_header_base64).unwrap_or_default();
let splitted_header: Vec<&str> = decoded_header.split(':').collect();
if splitted_header.len() != 2 {
px_debug!("Malformed {} header: {}", TELEMETRY_HEADER, decoded_header);
return;
}
let Some(timestamp_str) = splitted_header.first() else {
px_debug!("Missing timestamp in {} header", TELEMETRY_HEADER);
return;
};
let Some(hmac_str) = splitted_header.get(1) else {
px_debug!("Missing hmac in {} header", TELEMETRY_HEADER);
return;
};
if !pxcrypto::is_hmac_valid(timestamp_str, hmac_str, &conf.cookie_secret) {
px_debug!(
"{} hmac validation failed. original hmac: {}, timestamp: {}.",
TELEMETRY_HEADER,
hmac_str,
timestamp_str
);
return;
}
let current_timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64;
let parsed_timestamp = timestamp_str.parse::<i64>().unwrap_or_default();
if parsed_timestamp == 0 || parsed_timestamp < current_timestamp {
px_debug!(
"{} timestamp expired: {} < {}",
TELEMETRY_HEADER,
parsed_timestamp,
current_timestamp
);
return;
}
let telemetry_result = send_telemetry_activity(conf, ctx, TelemetryUpdateReason::Command);
match telemetry_result {
Ok(response) => {
px_debug!("telemetry status: {}", response.get_status());
}
Err(e) => {
px_debug!("error sending telemetry: {}", e);
}
}
}