webserver_base/
axum_plausible_analytics.rsuse crate::{BaseSettings, Environment};
use axum::http::{HeaderMap, StatusCode};
use plausible_rs::{EventHeaders, EventPayload, Plausible, PAGEVIEW_EVENT};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use tracing::{error, info, instrument, warn};
#[derive(Debug, Serialize, Deserialize)]
pub struct RequestPayload {
pub user_agent: String,
pub url: String,
pub referrer: String,
pub screen_width: usize,
}
pub struct AxumPlausibleAnalyticsHandler {
plausible_client: Plausible,
}
impl AxumPlausibleAnalyticsHandler {
#[must_use]
pub fn new_with_client(http_client: Client) -> Self {
Self {
plausible_client: Plausible::new_with_client(http_client),
}
}
#[instrument(skip_all)]
pub async fn handle(
self: Arc<Self>,
headers: HeaderMap,
settings: BaseSettings,
addr: SocketAddr,
incoming_payload: RequestPayload,
) -> StatusCode {
let domain: String = if settings.environment == Environment::Production {
settings.analytics_domain.clone()
} else {
String::from("test.toddgriffin.me")
};
let outgoing_payload: EventPayload = EventPayload::builder(
domain,
PAGEVIEW_EVENT.to_string(),
incoming_payload.url.clone(),
)
.referrer(incoming_payload.referrer.clone())
.screen_width(incoming_payload.screen_width)
.build();
let real_client_ip: String = Self::resolve_true_client_ip_address(addr, headers);
let headers: EventHeaders =
EventHeaders::new(incoming_payload.user_agent.clone(), real_client_ip);
info!(
"Making Plausible Analytics calls with headers={:?} and body={:?}",
headers.clone(),
outgoing_payload.clone()
);
match self.plausible_client.event(headers, outgoing_payload).await {
Ok(bytes) => {
info!(
"Plausible Analytics call was a success: {}",
String::from_utf8_lossy(&bytes)
);
StatusCode::OK
}
Err(e) => {
error!("Failed Plausible Analytics call: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
}
}
}
#[instrument(skip_all)]
fn resolve_true_client_ip_address(socket_addr: SocketAddr, header_map: HeaderMap) -> String {
let prioritized_headers: Vec<&str> = vec![
"True-Client-IP",
"CF-Connecting-IP",
"X-Forwarded-For",
"X-Real-IP",
"Forwarded",
"socket_addr",
];
let prioritized_headers_values: HashMap<&str, Option<String>> = prioritized_headers.iter().map(|prioritized_header| {
if *prioritized_header == "socket_addr" {
return (*prioritized_header, Some(socket_addr.ip().to_string()));
}
let header_value: Option<String> = match header_map.get(*prioritized_header) {
Some(header_value) => {
match header_value.to_str() {
Ok(header_value) => {
if *prioritized_header == "X-Forwarded-For" {
info!("full HTTP 'X-Forwarded-For' header IP list: {header_value}");
let parts: Vec<&str> = header_value.split(',').collect();
let x_forwarded_for_client_ip: Option<&&str> = parts.first();
if let Some(x_forwarded_for_client_ip) = x_forwarded_for_client_ip {
let x_forwarded_for: String = (*x_forwarded_for_client_ip).to_string();
Some(x_forwarded_for)
} else {
None
}
} else {
Some(header_value.to_string())
}
}
Err(to_str_error) => {
error!("failed to parse HTTP '{prioritized_header}' header value: {to_str_error}");
None
}
}
}
None => {
None
},
};
(*prioritized_header, header_value)
}).collect();
info!(
"Client IP headers: {:?}",
prioritized_headers_values.clone()
);
for prioritized_header in prioritized_headers {
if let Some(Some(header_value)) = prioritized_headers_values.get(prioritized_header) {
info!(
"chose HTTP '{prioritized_header}' header for true client IP: '{header_value}'"
);
return header_value.to_string();
}
}
error!("failed to find any prioritized HTTP headers for true client IP address");
prioritized_headers_values
.get("socket_addr")
.unwrap()
.as_ref()
.unwrap()
.to_string()
}
}