use core::fmt;
use core::future::{ready, Future};
use serde::Deserialize;
use skyzen_core::Extractor;
use crate::StatusCode;
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct CfBotManagement {
pub score: Option<u32>,
pub verified_bot: Option<bool>,
pub corporate_proxy: Option<bool>,
pub static_resource: Option<bool>,
pub ja3_hash: Option<String>,
pub ja4: Option<String>,
#[serde(deserialize_with = "sequence_or_map_values")]
pub detection_ids: Vec<u32>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct CfProperties {
pub colo: Option<String>,
pub country: Option<String>,
pub city: Option<String>,
pub region: Option<String>,
pub region_code: Option<String>,
pub continent: Option<String>,
pub latitude: Option<String>,
pub longitude: Option<String>,
pub timezone: Option<String>,
pub postal_code: Option<String>,
pub metro_code: Option<String>,
pub asn: Option<u32>,
pub as_organization: Option<String>,
pub http_protocol: Option<String>,
pub tls_version: Option<String>,
pub tls_cipher: Option<String>,
pub bot_management: Option<CfBotManagement>,
#[serde(skip)]
pub raw: serde_json::Value,
}
#[derive(Debug)]
pub struct CfPropertiesUnavailable(&'static str, Option<String>);
impl fmt::Display for CfPropertiesUnavailable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.0)?;
self.1
.as_ref()
.map_or(Ok(()), |detail| write!(f, ": {detail}"))
}
}
impl std::error::Error for CfPropertiesUnavailable {}
impl http_kit::HttpError for CfPropertiesUnavailable {
fn status(&self) -> StatusCode {
StatusCode::INTERNAL_SERVER_ERROR
}
}
#[derive(Debug, Clone)]
pub struct CfPropertiesSlot(pub Result<CfProperties, String>);
impl Extractor for CfProperties {
type Error = CfPropertiesUnavailable;
fn extract(
request: &mut crate::Request,
) -> impl Future<Output = Result<Self, Self::Error>> + Send {
ready(match request.extensions().get::<CfPropertiesSlot>() {
Some(CfPropertiesSlot(Ok(properties))) => Ok(properties.clone()),
Some(CfPropertiesSlot(Err(error))) => Err(CfPropertiesUnavailable(
"the runtime sent a `request.cf` object this build could not decode",
Some(error.clone()),
)),
None => Err(CfPropertiesUnavailable(
"this request carried no `request.cf`; it is set by Cloudflare and absent on any \
other WinterCG host",
None,
)),
})
}
}
impl CfPropertiesSlot {
#[cfg(target_arch = "wasm32")]
pub fn read(request: &web_sys::Request) -> Result<Option<Self>, wasm_bindgen::JsValue> {
use wasm_bindgen::JsValue;
let cf = js_sys::Reflect::get(request.as_ref(), &JsValue::from_str("cf"))?;
if cf.is_undefined() || cf.is_null() {
return Ok(None);
}
let properties = CfProperties::decode(cf);
if let Err(error) = &properties {
tracing::error!(error, "failed to decode `request.cf`");
}
Ok(Some(Self(properties)))
}
}
fn sequence_or_map_values<'de, D>(deserializer: D) -> Result<Vec<u32>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{IgnoredAny, MapAccess, SeqAccess, Visitor};
struct Ids;
impl<'de> Visitor<'de> for Ids {
type Value = Vec<u32>;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a sequence of detection ids, or the placeholder map workerd sends")
}
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut ids = Vec::new();
while let Some(id) = seq.next_element()? {
ids.push(id);
}
Ok(ids)
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
let mut ids = Vec::new();
while let Some((IgnoredAny, id)) = map.next_entry()? {
ids.push(id);
}
Ok(ids)
}
}
deserializer.deserialize_any(Ids)
}
impl CfProperties {
#[cfg(target_arch = "wasm32")]
fn decode(cf: wasm_bindgen::JsValue) -> Result<Self, String> {
let raw: serde_json::Value = serde_wasm_bindgen::from_value(cf)
.map_err(|error| format!("`request.cf` is not a plain JSON object: {error}"))?;
let mut properties: Self = serde_json::from_value(raw.clone())
.map_err(|error| format!("`request.cf` has an unexpected field type: {error}"))?;
properties.raw = raw;
Ok(properties)
}
}
#[cfg(test)]
mod tests {
use super::CfBotManagement;
#[test]
fn detection_ids_decode_from_the_documented_sequence() {
let bm: CfBotManagement =
serde_json::from_str(r#"{"score":99,"detectionIds":[7,11]}"#).unwrap();
assert_eq!(bm.detection_ids, vec![7, 11]);
}
#[test]
fn detection_ids_decode_from_workerds_placeholder_map() {
let bm: CfBotManagement =
serde_json::from_str(r#"{"score":99,"detectionIds":{}}"#).unwrap();
assert!(bm.detection_ids.is_empty());
let bm: CfBotManagement =
serde_json::from_str(r#"{"detectionIds":{"a":3,"b":5}}"#).unwrap();
assert_eq!(bm.detection_ids, vec![3, 5]);
}
}