use std::collections::HashMap;
use std::io::Read;
use std::time::Duration as StdDuration;
use chrono::{DateTime, Duration, Utc};
use cookie::time::OffsetDateTime;
use cookie::{Cookie, SameSite};
use http::header::{COOKIE, SET_COOKIE};
use http::response::Parts;
use http::{HeaderValue, Method};
use serde::{Deserialize, Serialize};
use serde_json::Error;
use uuid::Uuid;
use super::crypto::{decrypt, encrypt};
use crate::config;
use crate::proxy::compute::data_collection::payload::{Payload, User};
use crate::proxy::context::incoming::RequestHandle;
use crate::proxy::DATA_COLLECTION_ENDPOINT_FROM_THIRD_PARTY_SDK;
static SESSION_DURATION: Duration = Duration::minutes(30);
#[derive(Serialize, Deserialize, Debug)]
pub struct EdgeeCookie {
pub id: Uuid, pub fs: DateTime<Utc>, pub ls: DateTime<Utc>, pub ss: DateTime<Utc>, #[serde(skip_serializing)]
pub ps: Option<DateTime<Utc>>, pub sc: u32, #[serde(skip_serializing_if = "Option::is_none")]
pub sz: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
pub uaa: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
pub uab: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
pub uam: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
pub uapv: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
pub uafvl: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
pub tz: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
pub c: Option<String>, }
impl EdgeeCookie {
pub fn new() -> EdgeeCookie {
let now = Utc::now();
EdgeeCookie {
id: Uuid::new_v4(),
fs: now,
ls: now,
ss: now,
ps: None,
sc: 1,
sz: None,
uaa: None,
uab: None,
uam: None,
uapv: None,
uafvl: None,
tz: None,
c: None,
}
}
fn set_screen_size(&mut self, payload: &Payload) {
if let Some(client) = payload
.data_collection
.as_ref()
.and_then(|dc| dc.context.as_ref())
.and_then(|ctx| ctx.client.as_ref())
{
let screen_width = client.screen_width;
let screen_height = client.screen_height;
let screen_density = client.screen_density;
if screen_width.is_none() || screen_height.is_none() || screen_density.is_none() {
return;
}
self.sz = Some(format!(
"{}x{}x{}",
screen_width.unwrap(),
screen_height.unwrap(),
screen_density.unwrap()
));
}
}
fn set_client_hints(&mut self, payload: &Payload) {
if let Some(client) = payload
.data_collection
.as_ref()
.and_then(|dc| dc.context.as_ref())
.and_then(|ctx| ctx.client.as_ref())
{
if client.user_agent_architecture.is_some() {
self.uaa = client.user_agent_architecture.clone();
}
if client.user_agent_bitness.is_some() {
self.uab = client.user_agent_bitness.clone();
}
if client.user_agent_model.is_some() {
self.uam = client.user_agent_model.clone();
}
if client.os_version.is_some() {
self.uapv = client.os_version.clone();
}
if client.user_agent_full_version_list.is_some() {
self.uafvl = client.user_agent_full_version_list.clone();
}
if client.timezone.is_some() {
self.tz = client.timezone.clone();
}
}
}
fn set_consent(&mut self, payload: &Payload) {
if let Some(consent) = payload
.data_collection
.as_ref()
.and_then(|dc| dc.consent.as_ref())
{
self.c = Some(consent.to_string());
}
}
}
pub fn get_or_set(request: &RequestHandle, response: &mut Parts, payload: &Payload) -> EdgeeCookie {
let edgee_cookie = get(request, response, payload);
if edgee_cookie.is_none() {
return init_and_set_cookie(request, response, payload);
}
edgee_cookie.unwrap()
}
pub fn has_cookie(request: &RequestHandle) -> bool {
get_cookie(config::get().compute.cookie_name.as_str(), request).is_some()
}
pub fn get_cookie(name: &str, request: &RequestHandle) -> Option<String> {
let all_cookies = request.get_headers().get_all(COOKIE);
for cookie in all_cookies {
let mut map = HashMap::new();
for item in cookie.to_str().unwrap().split("; ") {
if let Some((key, value)) = item.split_once('=') {
map.insert(key.trim().to_string(), value.trim().to_string());
}
}
if map.contains_key(name) {
return Some(map.get(name).unwrap().to_string());
}
}
get_cookie_from_query_or_none(name, request)
}
fn get_cookie_from_query_or_none(name: &str, request: &RequestHandle) -> Option<String> {
if request.get_path() == DATA_COLLECTION_ENDPOINT_FROM_THIRD_PARTY_SDK
&& request.get_method() == Method::POST
{
let mut param_name = "e";
if name == format!("{}_u", config::get().compute.cookie_name) {
param_name = "u";
}
let map: HashMap<String, String> = request
.get_query()
.split('&')
.map(|s| s.split('=').collect::<Vec<&str>>())
.filter(|v| v.len() == 2)
.map(|v| (v[0].to_string(), v[1].to_string()))
.collect();
let e = map.get(param_name);
e.map(|e| e.to_string())
} else {
None
}
}
pub fn get(
request: &RequestHandle,
response: &mut Parts,
payload: &Payload,
) -> Option<EdgeeCookie> {
if let Some(value) = get_cookie(config::get().compute.cookie_name.as_str(), request) {
let edgee_cookie_result = decrypt_and_update(value.as_str(), payload);
if edgee_cookie_result.is_err() {
return Some(init_and_set_cookie(request, response, payload));
}
let mut edgee_cookie = edgee_cookie_result.unwrap();
edgee_cookie.set_screen_size(payload);
edgee_cookie.set_client_hints(payload);
edgee_cookie.set_consent(payload);
let edgee_cookie_str = serde_json::to_string(&edgee_cookie).unwrap();
let edgee_cookie_encrypted = encrypt(&edgee_cookie_str).unwrap();
set_cookie(
config::get().compute.cookie_name.as_str(),
&edgee_cookie_encrypted,
response,
request.get_host().as_str(),
);
return Some(edgee_cookie);
}
None
}
fn decrypt_and_update(value: &str, payload: &Payload) -> Result<EdgeeCookie, &'static str> {
let decrypted = decrypt(value);
if decrypted.is_err() {
return Err("Failed to decrypt EdgeeCookie");
}
let edgee_cookie_str = parse(decrypted?.as_bytes());
if edgee_cookie_str.is_err() {
return Err("Failed to parse EdgeeCookie");
}
let mut edgee_cookie = edgee_cookie_str.unwrap();
if Utc::now().signed_duration_since(edgee_cookie.ls) < SESSION_DURATION {
edgee_cookie.ls = Utc::now();
}
if Utc::now().signed_duration_since(edgee_cookie.ls) >= SESSION_DURATION {
edgee_cookie.sc += 1;
edgee_cookie.ps = Some(edgee_cookie.ss);
let now = Utc::now();
edgee_cookie.ls = now;
edgee_cookie.ss = now;
}
edgee_cookie.set_screen_size(payload);
edgee_cookie.set_client_hints(payload);
edgee_cookie.set_consent(payload);
Ok(edgee_cookie)
}
fn init_and_set_cookie(
request: &RequestHandle,
response: &mut Parts,
payload: &Payload,
) -> EdgeeCookie {
let mut edgee_cookie = EdgeeCookie::new();
edgee_cookie.set_screen_size(payload);
edgee_cookie.set_client_hints(payload);
edgee_cookie.set_consent(payload);
let edgee_cookie_str = serde_json::to_string(&edgee_cookie).unwrap();
let edgee_cookie_encrypted = encrypt(&edgee_cookie_str).unwrap();
set_cookie(
config::get().compute.cookie_name.as_str(),
&edgee_cookie_encrypted,
response,
request.get_host().as_str(),
);
edgee_cookie
}
fn set_cookie(name: &str, value: &str, response: &mut Parts, host: &str) {
let secure = config::get().http.is_some() && config::get().http.as_ref().unwrap().force_https;
let cookie_domain = config::get()
.compute
.cookie_domain
.clone()
.unwrap_or_else(|| get_root_domain(host));
let cookie = Cookie::build((name, value))
.domain(cookie_domain)
.path("/")
.http_only(false)
.secure(secure)
.same_site(SameSite::Lax)
.expires(OffsetDateTime::now_utc() + StdDuration::from_secs(365 * 24 * 60 * 60));
response.headers.append(
SET_COOKIE,
HeaderValue::from_str(cookie.to_string().as_str()).unwrap(),
);
}
pub fn get_root_domain(host: &str) -> String {
let domain_result = addr::parse_domain_name(host);
match domain_result {
Ok(domain) => {
if let Some(domain) = domain.root() {
return domain.to_string();
}
host.to_string()
}
Err(_) => host.to_string(),
}
}
fn parse<T: Read>(clean_json: T) -> Result<EdgeeCookie, Error> {
serde_json::from_reader(clean_json)
}
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct EdgeeUserCookie {
pub user_id: Option<String>,
pub anonymous_id: Option<String>,
pub properties: Option<HashMap<String, serde_json::Value>>,
}
pub fn set_user_cookie(request: &RequestHandle, response: &mut Parts, user_data: &User) {
let user_cookie = EdgeeUserCookie {
user_id: user_data.user_id.clone(),
anonymous_id: user_data.anonymous_id.clone(),
properties: user_data.properties.clone(),
};
let user_cookie_str = serde_json::to_string(&user_cookie).unwrap();
let encrypted_user_cookie = encrypt(&user_cookie_str).unwrap();
let cookie_name = format!("{}_u", config::get().compute.cookie_name);
set_cookie(
&cookie_name,
&encrypted_user_cookie,
response,
request.get_host().as_str(),
);
}
pub fn get_user_cookie(request: &RequestHandle) -> Option<EdgeeUserCookie> {
let cookie_name = format!("{}_u", config::get().compute.cookie_name);
if let Some(value) = get_cookie(cookie_name.as_str(), request) {
let cookie_result = decrypt_user_cookie(value.as_str());
if cookie_result.is_err() {
return None;
}
return Some(cookie_result.unwrap());
}
None
}
fn decrypt_user_cookie(value: &str) -> Result<EdgeeUserCookie, &'static str> {
let decrypted = decrypt(value);
if decrypted.is_err() {
return Err("Failed to decrypt EdgeeUserCookie");
}
let cookie_str: Result<EdgeeUserCookie, Error> = parse_user_cookie(decrypted?.as_bytes());
if cookie_str.is_err() {
return Err("Failed to parse EdgeeUserCookie");
}
Ok(cookie_str.unwrap())
}
fn parse_user_cookie<T: Read>(clean_json: T) -> Result<EdgeeUserCookie, Error> {
serde_json::from_reader(clean_json)
}
#[cfg(test)]
mod tests {
use super::*;
use http::response;
use pretty_assertions::assert_eq;
use serde_json::json;
fn sample_user_data() -> User {
User {
user_id: Some("123".to_string()),
anonymous_id: Some("456".to_string()),
edgee_id: "456".to_string(),
properties: Some(HashMap::from([
("prop1".to_string(), json!(true)),
("prop2".to_string(), json!(false)),
("prop3".to_string(), json!(10)),
("prop4".to_string(), json!("ok")),
])),
}
}
#[test]
fn test_get_root_domain() {
assert_eq!(get_root_domain("example.com"), "example.com");
assert_eq!(get_root_domain("test.example.com"), "example.com");
assert_eq!(get_root_domain("sub.test.example.com"), "example.com");
assert_eq!(get_root_domain("localhost"), "localhost");
}
#[test]
fn test_set_user_cookie() {
let request = RequestHandle::default();
let response = response::Builder::new().status(200).body("").unwrap();
let (mut parts, _body) = response.into_parts();
set_user_cookie(&request, &mut parts, &sample_user_data());
assert_eq!(parts.headers.len(), 1);
}
}