use crate::{Ja4Fingerprint, Plugin, get_request_host, now_ms};
use ahash::AHashMap;
use bytes::BytesMut;
use http::StatusCode;
use http::{HeaderName, HeaderValue};
#[cfg(feature = "tracing")]
use opentelemetry::{
Context,
global::{BoxedSpan, BoxedTracer, ObjectSafeSpan},
trace::{SpanKind, TraceContextExt, Tracer},
};
use pingora::cache::CacheKey;
use pingora::http::RequestHeader;
use pingora::protocols::Digest;
use pingora::protocols::TimingDigest;
use pingora::proxy::Session;
use pingora_limits::inflight::Guard;
use std::any::Any;
use std::borrow::Cow;
use std::fmt::Write;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::{Duration, Instant, SystemTime};
use strum::EnumString;
const SECOND: u64 = 1_000;
const MINUTE: u64 = 60 * SECOND;
const HOUR: u64 = 60 * MINUTE;
#[inline]
pub fn format_duration(buf: &mut BytesMut, ms: u64) {
if ms < SECOND {
buf.extend_from_slice(itoa::Buffer::new().format(ms).as_bytes());
buf.extend_from_slice(b"ms");
} else if ms < MINUTE {
buf.extend_from_slice(
itoa::Buffer::new().format(ms / SECOND).as_bytes(),
);
let value = (ms % SECOND) / 100;
if value != 0 {
buf.extend_from_slice(b".");
buf.extend_from_slice(itoa::Buffer::new().format(value).as_bytes());
}
buf.extend_from_slice(b"s");
} else if ms < HOUR {
buf.extend_from_slice(
itoa::Buffer::new().format(ms / MINUTE).as_bytes(),
);
let value = ms % MINUTE * 10 / MINUTE;
if value != 0 {
buf.extend_from_slice(b".");
buf.extend_from_slice(itoa::Buffer::new().format(value).as_bytes());
}
buf.extend_from_slice(b"m");
} else {
buf.extend_from_slice(itoa::Buffer::new().format(ms / HOUR).as_bytes());
let value = ms % HOUR * 10 / HOUR;
if value != 0 {
buf.extend_from_slice(b".");
buf.extend_from_slice(itoa::Buffer::new().format(value).as_bytes());
}
buf.extend_from_slice(b"h");
}
}
#[derive(PartialEq)]
pub enum ModifiedMode {
Upstream,
Response,
}
impl From<&str> for ModifiedMode {
fn from(value: &str) -> Self {
match value {
"upstream" => ModifiedMode::Upstream,
_ => ModifiedMode::Response,
}
}
}
pub trait ModifyResponseBody: Sync + Send {
fn handle(
&mut self,
session: &Session,
body: &mut Option<bytes::Bytes>,
end_of_stream: bool,
) -> pingora::Result<()>;
fn name(&self) -> &str {
"unknown"
}
}
pub trait HandleRequestBody: Sync + Send {
fn handle(
&mut self,
body: Option<&bytes::Bytes>,
end_of_stream: bool,
) -> pingora::Result<()>;
fn restart(&mut self);
}
#[derive(Default)]
pub struct ConnectionInfo {
pub id: usize,
pub client_ip: Option<String>,
pub remote_addr: Option<String>,
pub remote_port: Option<u16>,
pub server_addr: Option<String>,
pub server_port: Option<u16>,
pub tls_version: Option<Cow<'static, str>>,
pub tls_cipher: Option<Cow<'static, str>>,
pub ja4: Option<Arc<Ja4Fingerprint>>,
pub tls_client_cert: Option<Arc<TlsClientCert>>,
pub reused: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TlsClientCert {
pub subject: String,
pub fingerprint: String,
pub serial: String,
}
pub struct Timing {
pub created_at: Instant,
pub connection_duration: u64,
pub tls_handshake: Option<i32>,
pub upstream_connect: Option<i32>,
pub upstream_tcp_connect: Option<i32>,
pub upstream_tls_handshake: Option<i32>,
pub upstream_connect_offload_wait: Option<i32>,
pub upstream_processing: Option<i32>,
pub upstream_response: Option<i32>,
pub upstream_first_body: Option<i32>,
pub upstream_connection_duration: Option<u64>,
pub cache_lookup: Option<i32>,
pub cache_lock: Option<i32>,
}
impl Default for Timing {
fn default() -> Self {
Self {
created_at: Instant::now(),
connection_duration: 0,
tls_handshake: None,
upstream_connect: None,
upstream_tcp_connect: None,
upstream_tls_handshake: None,
upstream_connect_offload_wait: None,
upstream_processing: None,
upstream_response: None,
upstream_first_body: None,
upstream_connection_duration: None,
cache_lookup: None,
cache_lock: None,
}
}
}
pub trait UpstreamInstance: Send + Sync {
fn on_transport_failure(&self, address: &str);
fn on_response(&self, address: &str, status: StatusCode);
fn completed(&self) -> i32;
}
pub trait LocationInstance: Send + Sync {
fn name(&self) -> &str;
fn upstream(&self) -> &str;
fn fallback_upstream(&self) -> &str {
""
}
fn has_rewrite(&self) -> bool {
false
}
fn rewrite(
&self,
header: &mut RequestHeader,
variables: &mut Option<AHashMap<String, String>>,
) -> pingora::Result<bool>;
fn headers(&self) -> Option<&Vec<(HeaderName, HeaderValue, bool)>>;
fn client_body_size_limit(&self) -> usize;
fn on_request(&self) -> pingora::Result<(u64, i32)>;
fn on_response(&self);
fn apply_timeouts(
&self,
_options: &mut pingora::upstreams::peer::PeerOptions,
) {
}
}
#[derive(Default)]
pub struct UpstreamInfo {
pub upstream_instance: Option<Arc<dyn UpstreamInstance>>,
pub location_instance: Option<Arc<dyn LocationInstance>>,
pub location: Arc<str>,
pub name: Arc<str>,
pub address: String,
pub reused: bool,
pub processing_count: Option<i32>,
pub connected_count: Option<i32>,
pub status: Option<StatusCode>,
pub retries: u8,
pub fallback: bool,
pub fallback_from: Option<Arc<str>>,
pub max_retries: Option<u8>,
pub max_retry_window: Option<Duration>,
pub failed_addresses: Vec<String>,
pub backend_inflight: Option<InflightGuard>,
pub sticky_cookie: Option<String>,
}
pub struct InflightGuard(Arc<AtomicU32>);
impl InflightGuard {
pub fn new(count: Arc<AtomicU32>) -> Self {
count.fetch_add(1, Ordering::Relaxed);
Self(count)
}
}
impl Drop for InflightGuard {
fn drop(&mut self) {
self.0.fetch_sub(1, Ordering::Relaxed);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RateLimitQuota {
pub limit: u64,
pub remaining: u64,
pub reset: Option<u64>,
}
#[derive(Default)]
pub struct RequestState {
pub request_id: Option<String>,
pub status: Option<StatusCode>,
pub payload_size: usize,
pub guards: Vec<Guard>,
pub processing_count: i32,
pub accepted_count: u64,
pub location_processing_count: i32,
pub location_accepted_count: u64,
pub admin: bool,
pub proxying: bool,
pub stale_for_status: bool,
pub body_end_said: bool,
pub rate_limit: Option<RateLimitQuota>,
}
#[derive(Clone, Debug)]
pub struct CacheKeyVariant {
pub at: usize,
pub len: usize,
pub alternatives: Arc<Vec<Vec<String>>>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CacheQueryRule {
Ignore(Vec<String>),
Allow(Vec<String>),
}
impl CacheQueryRule {
pub fn key_query(&self, query: &str) -> String {
fn name(pair: &str) -> &str {
pair.split('=').next().unwrap_or(pair)
}
let mut pairs: Vec<&str> = query
.split('&')
.filter(|pair| !pair.is_empty())
.filter(|pair| {
let name = name(pair);
match self {
Self::Ignore(names) => !names.iter().any(|n| n == name),
Self::Allow(names) => names.iter().any(|n| n == name),
}
})
.collect();
pairs.sort_by(|a, b| name(a).cmp(name(b)));
pairs.join("&")
}
}
#[derive(Default)]
pub struct CacheInfo {
pub namespace: Option<String>,
pub keys: Option<Vec<String>>,
pub key_variants: Option<Vec<CacheKeyVariant>>,
pub check_cache_control: bool,
pub max_ttl: Option<Duration>,
pub vary_headers: Option<Arc<Vec<String>>>,
pub default_ttl: Option<Duration>,
pub status_ttl: Option<Arc<Vec<(u16, Duration)>>>,
pub query_rule: Option<Arc<CacheQueryRule>>,
pub key_query: Option<String>,
pub key_headers: Option<Arc<Vec<String>>>,
pub revalidate: bool,
pub reading_count: Option<u32>,
pub writing_count: Option<u32>,
}
pub fn cache_key_slot(value: &str) -> String {
if !value.contains([':', '%']) {
return value.to_string();
}
value.replace('%', "%25").replace(':', "%3A")
}
fn header_key_slot(header: &RequestHeader, name: &str) -> String {
let mut values = header.headers.get_all(name).iter();
let Some(first) = values.next() else {
return String::new();
};
let mut value = String::from_utf8_lossy(first.as_bytes()).into_owned();
for more in values {
value.push_str(", ");
value.push_str(&String::from_utf8_lossy(more.as_bytes()));
}
cache_key_slot(&value)
}
impl CacheInfo {
fn header_key_components(&self, header: &RequestHeader) -> Vec<String> {
let Some(names) = &self.key_headers else {
return vec![];
};
let mut slots: Vec<String> = names
.iter()
.map(|name| header_key_slot(header, name))
.collect();
if slots.iter().all(|slot| slot.is_empty()) {
slots.clear();
}
slots
}
pub fn settle_key_query(&mut self, header: &RequestHeader) {
if let Some(rule) = &self.query_rule {
self.key_query =
Some(rule.key_query(header.uri.query().unwrap_or_default()));
}
}
pub fn ask_with_key_query(&self, header: &mut RequestHeader) -> bool {
let made;
let query = match (&self.key_query, &self.query_rule) {
(Some(query), _) => query.as_str(),
(None, Some(rule)) => {
made = rule.key_query(header.uri.query().unwrap_or_default());
made.as_str()
},
(None, None) => return true,
};
if header.uri.query() == (!query.is_empty()).then_some(query) {
return true;
}
let path_and_query = if query.is_empty() {
header.uri.path().to_string()
} else {
format!("{}?{query}", header.uri.path())
};
let mut parts = header.uri.clone().into_parts();
let Ok(path_and_query) = path_and_query.parse() else {
return false;
};
parts.path_and_query = Some(path_and_query);
let Ok(uri) = http::Uri::from_parts(parts) else {
return false;
};
header.set_uri(uri);
true
}
}
#[derive(Debug, Clone)]
pub struct BodyPace {
rate: u64,
after: u64,
sent: u64,
limited_since: Option<Instant>,
}
impl BodyPace {
pub fn new(rate: u64, after: u64) -> Self {
Self {
rate: rate.max(1),
after,
sent: 0,
limited_since: None,
}
}
pub fn delay(&mut self, len: usize) -> Option<Duration> {
self.delay_at(len, Instant::now())
}
pub fn delay_before(&mut self, len: usize) -> Option<Duration> {
self.delay_before_at(len, Instant::now())
}
fn delay_at(&mut self, len: usize, now: Instant) -> Option<Duration> {
self.count(len, now);
self.owed(now)
}
fn delay_before_at(
&mut self,
len: usize,
now: Instant,
) -> Option<Duration> {
let owed = self.owed(now);
self.count(len, now);
owed
}
fn count(&mut self, len: usize, now: Instant) {
self.sent = self.sent.saturating_add(len as u64);
if self.sent > self.after {
self.limited_since.get_or_insert(now);
}
}
fn owed(&mut self, now: Instant) -> Option<Duration> {
let limited = self.sent.checked_sub(self.after).filter(|n| *n > 0)?;
let since = self.limited_since?;
let due = Duration::from_secs_f64(limited as f64 / self.rate as f64);
let taken = now.saturating_duration_since(since);
if taken > due {
self.limited_since = now.checked_sub(due);
return None;
}
Some(due - taken).filter(|delay| !delay.is_zero())
}
}
#[derive(Default)]
pub struct Features {
pub variables: Option<AHashMap<String, String>>,
pub original_uri: Option<http::Uri>,
pub plugin_processing_times: Option<Vec<(Arc<str>, u32)>>,
pub compression_stat: Option<CompressionStat>,
pub modify_body_handlers:
Option<AHashMap<String, Box<dyn ModifyResponseBody>>>,
pub plugin_notes: Option<Vec<(String, &'static str)>>,
pub plugin_values: Option<Vec<(String, Box<dyn Any + Send + Sync>)>>,
pub request_body_handlers: Option<Vec<Box<dyn HandleRequestBody>>>,
pub body_pace: Option<BodyPace>,
pub plain_response_body: Option<PlainBody>,
#[cfg(feature = "tracing")]
pub otel_tracer: Option<OtelTracer>,
#[cfg(feature = "tracing")]
pub upstream_span: Option<BoxedSpan>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum PlainBody {
Any,
Types(Vec<String>),
}
#[derive(Default)]
pub struct CompressionStat {
pub algorithm: String,
pub in_bytes: usize,
pub out_bytes: usize,
pub duration: Duration,
}
impl CompressionStat {
pub fn ratio(&self) -> f64 {
if self.out_bytes == 0 {
return 0.0;
}
(self.in_bytes as f64) / (self.out_bytes as f64)
}
}
#[cfg(feature = "tracing")]
pub struct OtelTracer {
pub tracer: BoxedTracer,
pub http_request_span: BoxedSpan,
}
#[cfg(feature = "tracing")]
impl OtelTracer {
#[inline]
pub fn new_upstream_span(&self, name: &str) -> BoxedSpan {
self.tracer
.span_builder(name.to_string())
.with_kind(SpanKind::Client)
.start_with_context(
&self.tracer,
&Context::current().with_remote_span_context(
self.http_request_span.span_context().clone(),
),
)
}
}
pub type NamedPlugin = (Arc<str>, Arc<dyn Plugin>);
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, strum::Display)]
#[strum(serialize_all = "snake_case")]
pub enum CtxLogField {
ConnectionId,
UpstreamReused,
UpstreamStatus,
UpstreamAddr,
UpstreamName,
Processing,
UpstreamConnected,
UpstreamConnectTime,
UpstreamConnectTimeHuman,
UpstreamProcessingTime,
UpstreamProcessingTimeHuman,
UpstreamResponseTime,
UpstreamResponseTimeHuman,
UpstreamFirstBodyTime,
UpstreamFirstBodyTimeHuman,
UpstreamTcpConnectTime,
UpstreamTcpConnectTimeHuman,
UpstreamTlsHandshakeTime,
UpstreamTlsHandshakeTimeHuman,
UpstreamConnectOffloadWaitTime,
UpstreamConnectOffloadWaitTimeHuman,
UpstreamConnectionTime,
UpstreamConnectionTimeHuman,
ConnectionTime,
ConnectionTimeHuman,
Location,
ConnectionReused,
TlsVersion,
TlsCipher,
TlsClientSubject,
TlsClientFingerprint,
TlsClientSerial,
TlsClientVerified,
Ja4,
#[strum(serialize = "ja4_r")]
Ja4R,
#[strum(serialize = "ja4_o")]
Ja4O,
#[strum(serialize = "ja4_ro")]
Ja4Ro,
TlsHandshakeTime,
TlsHandshakeTimeHuman,
CompressionTime,
CompressionTimeHuman,
CompressionRatio,
CacheLookupTime,
CacheLookupTimeHuman,
CacheLockTime,
CacheLockTimeHuman,
ServiceTime,
ServiceTimeHuman,
}
#[derive(Default)]
pub struct Ctx {
pub conn: ConnectionInfo,
pub upstream: UpstreamInfo,
pub timing: Timing,
pub state: RequestState,
pub cache: Option<Box<CacheInfo>>,
pub features: Option<Box<Features>>,
pub plugins: Option<Arc<[NamedPlugin]>>,
pub response_plugins: Option<Arc<[NamedPlugin]>>,
}
#[derive(Debug, Default)]
pub struct DigestDetail {
pub connection_reused: bool,
pub connection_time: u64,
pub tcp_established: u64,
pub tls_established: u64,
pub tcp_connect: Option<u64>,
pub tls_handshake: Option<u64>,
pub connect_offload_wait: Option<u64>,
pub tls_version: Option<Cow<'static, str>>,
pub tls_cipher: Option<Cow<'static, str>>,
pub tls_client_cert: Option<Arc<TlsClientCert>>,
}
#[inline]
pub(crate) fn timing_to_ms(timing: Option<&Option<TimingDigest>>) -> u64 {
match timing {
Some(Some(item)) => item
.established_ts
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64,
_ => 0,
}
}
#[inline]
pub fn get_digest_detail(digest: &Digest) -> DigestDetail {
let tcp_established = timing_to_ms(digest.timing_digest.first());
let mut connection_time = 0;
let now = now_ms();
if tcp_established > 0 && tcp_established < now {
connection_time = now - tcp_established;
}
let connection_reused = connection_time > 100;
let tcp_connect = establishment_ms(digest.timing_digest.first());
let connect_offload_wait = offload_wait_ms(digest.timing_digest.first());
let Some(ssl_digest) = &digest.ssl_digest else {
return DigestDetail {
connection_reused,
tcp_established,
connection_time,
tcp_connect,
connect_offload_wait,
..Default::default()
};
};
DigestDetail {
connection_reused,
tcp_established,
connection_time,
tcp_connect,
connect_offload_wait,
tls_established: timing_to_ms(digest.timing_digest.last()),
tls_handshake: establishment_ms(digest.timing_digest.last()),
tls_version: Some(ssl_digest.version.clone()),
tls_cipher: Some(ssl_digest.cipher.clone()),
tls_client_cert: ssl_digest
.extension
.get::<Arc<TlsClientCert>>()
.cloned(),
}
}
fn establishment_ms(timing: Option<&Option<TimingDigest>>) -> Option<u64> {
timing
.and_then(|item| item.as_ref())
.and_then(|item| item.establishment_duration)
.map(|duration| duration.as_millis() as u64)
}
fn offload_wait_ms(timing: Option<&Option<TimingDigest>>) -> Option<u64> {
timing
.and_then(|item| item.as_ref())
.and_then(|item| item.offload_wait_duration)
.map(|duration| duration.as_millis() as u64)
}
impl Ctx {
pub fn new() -> Self {
Self {
..Default::default()
}
}
#[inline]
pub fn add_variable(&mut self, key: &str, value: &str) {
let features = self.features.get_or_insert_default();
let variables = features.variables.get_or_insert_with(AHashMap::new);
variables.insert(key.to_string(), value.to_string());
}
#[inline]
pub fn extend_variables(&mut self, values: AHashMap<String, String>) {
let features = self.features.get_or_insert_default();
if let Some(variables) = features.variables.as_mut() {
variables.extend(values);
} else {
features.variables = Some(values);
}
}
#[inline]
pub fn get_variable(&self, key: &str) -> Option<&str> {
self.features
.as_ref()?
.variables
.as_ref()?
.get(key)
.map(|v| v.as_str())
}
#[inline]
pub fn set_plugin_note(&mut self, name: &str, note: &'static str) {
let notes = self
.features
.get_or_insert_default()
.plugin_notes
.get_or_insert_default();
match notes.iter_mut().find(|(key, _)| key == name) {
Some(found) => found.1 = note,
None => notes.push((name.to_string(), note)),
}
}
#[inline]
pub fn get_plugin_note(&self, name: &str) -> Option<&'static str> {
self.features
.as_ref()?
.plugin_notes
.as_ref()?
.iter()
.find(|(key, _)| key == name)
.map(|(_, note)| *note)
}
pub fn set_plugin_value<T: Any + Send + Sync>(
&mut self,
name: &str,
value: T,
) {
let values = self
.features
.get_or_insert_default()
.plugin_values
.get_or_insert_default();
match values.iter_mut().find(|(key, _)| key == name) {
Some(found) => found.1 = Box::new(value),
None => values.push((name.to_string(), Box::new(value))),
}
}
pub fn get_plugin_value<T: Any + Send + Sync>(
&self,
name: &str,
) -> Option<&T> {
self.features
.as_ref()?
.plugin_values
.as_ref()?
.iter()
.find(|(key, _)| key == name)
.and_then(|(_, value)| value.downcast_ref::<T>())
}
#[inline]
pub fn keep_response_body_plain(&mut self, types: Option<&[String]>) {
let wanted =
&mut self.features.get_or_insert_default().plain_response_body;
*wanted = match (wanted.take(), types) {
(Some(PlainBody::Any), _) | (_, None) => Some(PlainBody::Any),
(Some(PlainBody::Types(mut all)), Some(types)) => {
all.extend_from_slice(types);
Some(PlainBody::Types(all))
},
(None, Some(types)) => Some(PlainBody::Types(types.to_vec())),
};
}
#[inline]
pub fn response_body_stays_plain(
&self,
is_of: impl FnOnce(&[String]) -> bool,
) -> bool {
let wanted = self
.features
.as_ref()
.and_then(|features| features.plain_response_body.as_ref());
match wanted {
None => false,
Some(PlainBody::Any) => true,
Some(PlainBody::Types(types)) => is_of(types),
}
}
#[inline]
pub fn add_modify_body_handler(
&mut self,
name: &str,
handler: Box<dyn ModifyResponseBody>,
) {
let features = self.features.get_or_insert_default();
let handlers = features
.modify_body_handlers
.get_or_insert_with(AHashMap::new);
handlers.insert(name.to_string(), handler);
}
#[inline]
pub fn add_request_body_handler(
&mut self,
handler: Box<dyn HandleRequestBody>,
) {
self.features
.get_or_insert_default()
.request_body_handlers
.get_or_insert_default()
.push(handler);
}
#[inline]
pub fn get_modify_body_handler(
&mut self,
name: &str,
) -> Option<&mut Box<dyn ModifyResponseBody>> {
self.features
.as_mut()
.and_then(|f| f.modify_body_handlers.as_mut())
.and_then(|h| h.get_mut(name))
}
#[inline]
fn get_time_field(&self, field: Option<i32>) -> Option<u32> {
if let Some(value) = field
&& value >= 0
{
return Some(value as u32);
}
None
}
#[inline]
pub fn get_upstream_response_time(&self) -> Option<u32> {
self.get_time_field(self.timing.upstream_response)
}
#[inline]
pub fn get_upstream_connect_time(&self) -> Option<u32> {
self.get_time_field(self.timing.upstream_connect)
}
#[inline]
pub fn get_upstream_processing_time(&self) -> Option<u32> {
self.get_time_field(self.timing.upstream_processing)
}
#[inline]
pub fn get_upstream_first_body_time(&self) -> Option<u32> {
self.get_time_field(self.timing.upstream_first_body)
}
#[inline]
pub fn add_plugin_processing_time(&mut self, name: &Arc<str>, time: u32) {
let features = self.features.get_or_insert_default();
let times = features
.plugin_processing_times
.get_or_insert_with(|| Vec::with_capacity(5));
if let Some(item) = times.iter_mut().find(|item| &item.0 == name) {
item.1 += time;
} else {
times.push((Arc::clone(name), time));
}
}
#[inline]
pub fn append_log_value(&self, buf: &mut BytesMut, key: &str) {
if let Ok(field) = key.parse::<CtxLogField>() {
self.append_log_field(buf, field);
}
}
pub fn append_log_field(&self, buf: &mut BytesMut, field: CtxLogField) {
macro_rules! append_time {
($val:expr) => {
if let Some(ms) = $val {
buf.extend(itoa::Buffer::new().format(ms).as_bytes());
}
};
($val:expr, human) => {
if let Some(ms) = $val {
format_duration(buf, ms as u64);
}
};
}
match field {
CtxLogField::ConnectionId => {
buf.extend(itoa::Buffer::new().format(self.conn.id).as_bytes());
},
CtxLogField::UpstreamReused => {
if self.upstream.reused {
buf.extend(b"true");
} else {
buf.extend(b"false");
}
},
CtxLogField::UpstreamStatus => {
if let Some(status) = &self.upstream.status {
buf.extend_from_slice(status.as_str().as_bytes());
} else {
buf.extend_from_slice(b"-");
}
},
CtxLogField::UpstreamAddr => {
buf.extend(self.upstream.address.as_bytes())
},
CtxLogField::UpstreamName => {
buf.extend(self.upstream.name.as_bytes())
},
CtxLogField::Processing => buf.extend(
itoa::Buffer::new()
.format(self.state.processing_count)
.as_bytes(),
),
CtxLogField::UpstreamConnected => {
if let Some(value) = self.upstream.connected_count {
buf.extend(itoa::Buffer::new().format(value).as_bytes());
}
},
CtxLogField::UpstreamConnectTime => {
append_time!(self.get_upstream_connect_time())
},
CtxLogField::UpstreamConnectTimeHuman => {
append_time!(self.get_upstream_connect_time(), human)
},
CtxLogField::UpstreamProcessingTime => {
append_time!(self.get_upstream_processing_time())
},
CtxLogField::UpstreamProcessingTimeHuman => {
append_time!(self.get_upstream_processing_time(), human)
},
CtxLogField::UpstreamResponseTime => {
append_time!(self.get_upstream_response_time())
},
CtxLogField::UpstreamResponseTimeHuman => {
append_time!(self.get_upstream_response_time(), human)
},
CtxLogField::UpstreamFirstBodyTime => {
append_time!(self.get_upstream_first_body_time())
},
CtxLogField::UpstreamFirstBodyTimeHuman => {
append_time!(self.get_upstream_first_body_time(), human)
},
CtxLogField::UpstreamTcpConnectTime => {
append_time!(self.timing.upstream_tcp_connect)
},
CtxLogField::UpstreamTcpConnectTimeHuman => {
append_time!(self.timing.upstream_tcp_connect, human)
},
CtxLogField::UpstreamTlsHandshakeTime => {
append_time!(self.timing.upstream_tls_handshake)
},
CtxLogField::UpstreamTlsHandshakeTimeHuman => {
append_time!(self.timing.upstream_tls_handshake, human)
},
CtxLogField::UpstreamConnectOffloadWaitTime => {
append_time!(self.timing.upstream_connect_offload_wait)
},
CtxLogField::UpstreamConnectOffloadWaitTimeHuman => {
append_time!(self.timing.upstream_connect_offload_wait, human)
},
CtxLogField::UpstreamConnectionTime => {
append_time!(self.timing.upstream_connection_duration)
},
CtxLogField::UpstreamConnectionTimeHuman => {
append_time!(self.timing.upstream_connection_duration, human)
},
CtxLogField::ConnectionTime => {
append_time!(Some(self.timing.connection_duration))
},
CtxLogField::ConnectionTimeHuman => {
append_time!(Some(self.timing.connection_duration), human)
},
CtxLogField::Location => {
if !self.upstream.location.is_empty() {
buf.extend(self.upstream.location.as_bytes())
}
},
CtxLogField::ConnectionReused => {
if self.conn.reused {
buf.extend(b"true");
} else {
buf.extend(b"false");
}
},
CtxLogField::TlsVersion => {
if let Some(value) = &self.conn.tls_version {
buf.extend(value.as_bytes());
}
},
CtxLogField::TlsCipher => {
if let Some(value) = &self.conn.tls_cipher {
buf.extend(value.as_bytes());
}
},
CtxLogField::TlsClientSubject => {
if let Some(cert) = &self.conn.tls_client_cert {
buf.extend(cert.subject.as_bytes());
}
},
CtxLogField::TlsClientFingerprint => {
if let Some(cert) = &self.conn.tls_client_cert {
buf.extend(cert.fingerprint.as_bytes());
}
},
CtxLogField::TlsClientSerial => {
if let Some(cert) = &self.conn.tls_client_cert {
buf.extend(cert.serial.as_bytes());
}
},
CtxLogField::TlsClientVerified => {
if self.conn.tls_client_cert.is_some() {
buf.extend(b"true");
} else {
buf.extend(b"false");
}
},
CtxLogField::Ja4 => {
if let Some(fingerprint) = &self.conn.ja4 {
buf.extend(fingerprint.ja4().as_bytes());
}
},
CtxLogField::Ja4R => {
if let Some(fingerprint) = &self.conn.ja4 {
buf.extend(fingerprint.ja4_r().as_bytes());
}
},
CtxLogField::Ja4O => {
if let Some(fingerprint) = &self.conn.ja4 {
buf.extend(fingerprint.ja4_o().as_bytes());
}
},
CtxLogField::Ja4Ro => {
if let Some(fingerprint) = &self.conn.ja4 {
buf.extend(fingerprint.ja4_ro().as_bytes());
}
},
CtxLogField::TlsHandshakeTime => {
append_time!(self.timing.tls_handshake)
},
CtxLogField::TlsHandshakeTimeHuman => {
append_time!(self.timing.tls_handshake, human)
},
CtxLogField::CompressionTime => {
if let Some(feature) = &self.features
&& let Some(value) = &feature.compression_stat
{
append_time!(Some(value.duration.as_millis() as u64))
}
},
CtxLogField::CompressionTimeHuman => {
if let Some(feature) = &self.features
&& let Some(value) = &feature.compression_stat
{
append_time!(Some(value.duration.as_millis() as u64), human)
}
},
CtxLogField::CompressionRatio => {
if let Some(feature) = &self.features
&& let Some(value) = &feature.compression_stat
{
let tenths = (value.ratio() * 10.0).round() as u64;
buf.extend(
itoa::Buffer::new().format(tenths / 10).as_bytes(),
);
buf.extend_from_slice(b".");
buf.extend(
itoa::Buffer::new().format(tenths % 10).as_bytes(),
);
}
},
CtxLogField::CacheLookupTime => {
append_time!(self.timing.cache_lookup)
},
CtxLogField::CacheLookupTimeHuman => {
append_time!(self.timing.cache_lookup, human)
},
CtxLogField::CacheLockTime => {
append_time!(self.timing.cache_lock)
},
CtxLogField::CacheLockTimeHuman => {
append_time!(self.timing.cache_lock, human)
},
CtxLogField::ServiceTime => {
append_time!(Some(self.timing.created_at.elapsed().as_millis()))
},
CtxLogField::ServiceTimeHuman => {
append_time!(
Some(self.timing.created_at.elapsed().as_millis()),
human
)
},
}
}
pub fn generate_server_timing(&self) -> String {
let mut timing_str = String::with_capacity(200);
let mut first = true;
macro_rules! add_timing {
($name:expr, $dur:expr) => {
if !first {
timing_str.push_str(", ");
}
let _ = write!(&mut timing_str, "{};dur={}", $name, $dur);
first = false;
};
}
let mut upstream_time = 0;
if let Some(time) = self.get_upstream_connect_time() {
upstream_time += time;
add_timing!("upstream.connect", time);
}
if let Some(time) = self.get_upstream_processing_time() {
upstream_time += time;
add_timing!("upstream.processing", time);
}
if upstream_time > 0 {
add_timing!("upstream", upstream_time);
}
let mut cache_time = 0;
if let Some(time) = self.timing.cache_lookup {
cache_time += time;
add_timing!("cache.lookup", time);
}
if let Some(time) = self.timing.cache_lock {
cache_time += time;
add_timing!("cache.lock", time);
}
if cache_time > 0 {
add_timing!("cache", cache_time);
}
if let Some(features) = &self.features
&& let Some(times) = &features.plugin_processing_times
{
let mut plugin_time: u32 = 0;
for (name, time) in times {
if *time == 0 {
continue;
}
plugin_time += time;
if !first {
timing_str.push_str(", ");
}
let _ = write!(&mut timing_str, "plugin.{name};dur={time}");
first = false;
}
if plugin_time > 0 {
add_timing!("plugin", plugin_time);
}
}
let service_time = self.timing.created_at.elapsed().as_millis();
if !first {
timing_str.push_str(", ");
}
let _ = write!(&mut timing_str, "total;dur={}", service_time);
timing_str
}
#[inline]
pub fn push_cache_key(&mut self, key: String) {
let cache_info = self.cache.get_or_insert_default();
cache_info
.keys
.get_or_insert_with(|| Vec::with_capacity(2))
.push(key);
}
#[inline]
pub fn extend_cache_keys(&mut self, keys: Vec<String>) {
let cache_info = self.cache.get_or_insert_default();
cache_info
.keys
.get_or_insert_with(|| Vec::with_capacity(keys.len() + 2))
.extend(keys);
}
#[inline]
pub fn push_cache_key_variant(
&mut self,
current: Vec<String>,
alternatives: Arc<Vec<Vec<String>>>,
) {
let cache_info = self.cache.get_or_insert_default();
let keys = cache_info
.keys
.get_or_insert_with(|| Vec::with_capacity(current.len() + 2));
cache_info
.key_variants
.get_or_insert_default()
.push(CacheKeyVariant {
at: keys.len(),
len: current.len(),
alternatives,
});
keys.extend(current);
}
pub fn cache_key_alternatives(&self) -> Vec<Vec<String>> {
const MAX_ALTERNATIVES: usize = 1024;
let Some(cache_info) = &self.cache else {
return vec![vec![]];
};
let keys = cache_info.keys.as_deref().unwrap_or_default();
let variants = cache_info.key_variants.as_deref().unwrap_or_default();
let count = variants.iter().try_fold(1usize, |count, variant| {
count.checked_mul(variant.alternatives.len().max(1))
});
if variants.is_empty() {
return vec![keys.to_vec()];
}
if count.is_none_or(|n| n > MAX_ALTERNATIVES) {
tracing::warn!(
target: "pingap::core",
variants = variants.len(),
"too many cache key alternatives, only the key of this request is used"
);
return vec![keys.to_vec()];
}
let mut lists = vec![Vec::with_capacity(keys.len())];
let mut cursor = 0;
for variant in variants {
let fixed = keys.get(cursor..variant.at).unwrap_or_default();
lists = lists
.into_iter()
.flat_map(|list: Vec<String>| {
variant.alternatives.iter().map(move |alternative| {
let mut list = list.clone();
list.extend_from_slice(fixed);
list.extend_from_slice(alternative);
list
})
})
.collect();
cursor = variant.at + variant.len;
}
let rest = keys.get(cursor..).unwrap_or_default();
for list in lists.iter_mut() {
list.extend_from_slice(rest);
}
lists
}
#[inline]
pub fn update_upstream_timing_from_digest(
&mut self,
digest: &Digest,
reused: bool,
) {
let detail = get_digest_detail(digest);
self.timing.upstream_connection_duration = Some(detail.connection_time);
if reused {
return;
}
if let Some(tcp_connect) = detail.tcp_connect {
self.timing.upstream_tcp_connect = Some(tcp_connect as i32);
self.timing.upstream_tls_handshake =
detail.tls_handshake.map(|value| value as i32);
self.timing.upstream_connect_offload_wait =
detail.connect_offload_wait.map(|value| value as i32);
return;
}
let upstream_connect_time =
self.timing.upstream_connect.unwrap_or_default();
let mut upstream_tcp_connect = upstream_connect_time;
if detail.tls_established > detail.tcp_established {
let latency =
(detail.tls_established - detail.tcp_established) as i32;
upstream_tcp_connect -= latency;
self.timing.upstream_tls_handshake = Some(latency);
}
if upstream_tcp_connect > 0 {
self.timing.upstream_tcp_connect = Some(upstream_tcp_connect);
}
}
}
fn push_key_host(key: &mut String, host: &str) {
for byte in host.bytes() {
match byte {
b'a'..=b'z'
| b'0'..=b'9'
| b'.'
| b'-'
| b'_'
| b':'
| b'['
| b']' => key.push(byte as char),
b'A'..=b'Z' => key.push(byte.to_ascii_lowercase() as char),
_ => {
let _ = write!(key, "%{byte:02X}");
},
}
}
}
pub fn get_cache_key(
ctx: &Ctx,
method: &str,
header: &RequestHeader,
) -> CacheKey {
let Some(cache_info) = &ctx.cache else {
return CacheKey::new("", "");
};
let namespace = cache_info.namespace.as_ref().map_or("", |v| v);
let host = get_request_host(header).unwrap_or_default();
let path = header.uri.path();
let made;
let query = match (&cache_info.key_query, &cache_info.query_rule) {
(Some(query), _) => (!query.is_empty()).then_some(query.as_str()),
(None, Some(rule)) => {
made = rule.key_query(header.uri.query().unwrap_or_default());
(!made.is_empty()).then_some(made.as_str())
},
(None, None) => header.uri.query(),
};
let header_keys = cache_info.header_key_components(header);
let keys_len = cache_info
.keys
.iter()
.flatten()
.chain(&header_keys)
.map(|s| s.len() + 1)
.sum::<usize>();
let mut key_buf = String::with_capacity(
namespace.len()
+ keys_len
+ method.len()
+ 1
+ host.len()
+ path.len()
+ query.map_or(0, |query| query.len() + 1),
);
key_buf.push_str(namespace);
for k in cache_info.keys.iter().flatten().chain(&header_keys) {
key_buf.push_str(k);
key_buf.push(':');
}
key_buf.push_str(method);
key_buf.push(':');
push_key_host(&mut key_buf, host);
key_buf.push_str(path);
if let Some(query) = query {
key_buf.push('?');
key_buf.push_str(query);
}
CacheKey::new(key_buf, namespace)
}
#[cfg(test)]
mod tests {
use super::*;
use bytes::Bytes;
use bytes::BytesMut;
use http::Uri;
use pingora::cache::key::CacheHashKey;
use pingora::protocols::tls::SslDigest;
use pingora::protocols::tls::SslDigestExtension;
use pretty_assertions::assert_eq;
use std::{sync::Arc, time::Duration};
#[test]
fn test_body_pace() {
let start = Instant::now();
let at = |ms: u64| start + Duration::from_millis(ms);
let ms = |delay: Option<Duration>| delay.map(|d| d.as_millis());
let mut pace = BodyPace::new(1000, 0);
assert_eq!(Some(500), ms(pace.delay_at(500, at(0))));
assert_eq!(Some(500), ms(pace.delay_at(500, at(500))));
assert_eq!(None, ms(pace.delay_at(1000, at(5000))));
assert_eq!(Some(3000), ms(pace.delay_at(3000, at(5000))));
let mut pace = BodyPace::new(1000, 2000);
assert_eq!(None, ms(pace.delay_at(1500, at(0))));
assert_eq!(Some(500), ms(pace.delay_at(1000, at(10))));
assert_eq!(Some(1000), ms(pace.delay_at(1000, at(510))));
let mut pace = BodyPace::new(0, 0);
assert_eq!(Some(2000), ms(pace.delay_at(2, at(0))));
let mut pace = BodyPace::new(1000, 0);
assert_eq!(None, ms(pace.delay_at(0, at(0))));
let mut pace = BodyPace::new(1000, 0);
assert_eq!(None, ms(pace.delay_before_at(2000, at(0))));
assert_eq!(Some(2000), ms(pace.delay_before_at(500, at(0))));
assert_eq!(Some(500), ms(pace.delay_before_at(500, at(2000))));
assert_eq!(None, ms(pace.delay_before_at(500, at(3000))));
assert_eq!(None, ms(pace.delay_before_at(1000, at(9000))));
assert_eq!(Some(1000), ms(pace.delay_before_at(1, at(9000))));
let mut pace = BodyPace::new(1000, 2000);
assert_eq!(None, ms(pace.delay_before_at(1500, at(0))));
assert_eq!(None, ms(pace.delay_before_at(1000, at(0))));
assert_eq!(Some(500), ms(pace.delay_before_at(1000, at(0))));
}
#[test]
fn test_ask_with_key_query() {
let ask = |uri: &str, key_query: Option<&str>| {
let mut header = RequestHeader::build("GET", b"/", None).unwrap();
header.set_uri(uri.parse().unwrap());
let info = CacheInfo {
key_query: key_query.map(str::to_string),
..Default::default()
};
assert_eq!(true, info.ask_with_key_query(&mut header));
header.uri.to_string()
};
assert_eq!("/a?b=2&a=1", ask("/a?b=2&a=1", None));
assert_eq!(
"/a?a=1&b=2",
ask("/a?b=2&utm_source=x&a=1", Some("a=1&b=2"))
);
assert_eq!("/list", ask("/list?p%61ge=2", Some("")));
assert_eq!("/list", ask("/list?x=1;page=2", Some("")));
assert_eq!(
"/list?size=10",
ask("/list?Page=2&size=10", Some("size=10"))
);
assert_eq!("/a?a=1", ask("/a?a=1", Some("a=1")));
assert_eq!("/a", ask("/a", Some("")));
assert_eq!("/a", ask("/a?", Some("")));
let rule =
Arc::new(CacheQueryRule::Ignore(vec!["utm_source".to_string()]));
let mut info = CacheInfo {
query_rule: Some(rule),
..Default::default()
};
let mut header = RequestHeader::build("GET", b"/", None).unwrap();
header.set_uri("/a?b=2&utm_source=x&a=1".parse().unwrap());
let mut upstream = header.clone();
assert_eq!(true, info.ask_with_key_query(&mut upstream));
assert_eq!("/a?a=1&b=2", upstream.uri.to_string());
header.set_uri("/a?utm_source=x&a=1".parse().unwrap());
info.settle_key_query(&header);
assert_eq!(Some("a=1".to_string()), info.key_query);
let mut ctx = Ctx {
cache: Some(Box::new(info)),
..Default::default()
};
assert_eq!(
Some("GET:/a?a=1"),
get_cache_key(&ctx, "GET", &header).primary_key_str()
);
if let Some(info) = ctx.cache.as_mut() {
info.key_query = None;
}
header.set_uri("/a?z=9&utm_source=x&b=1".parse().unwrap());
assert_eq!(
Some("GET:/a?b=1&z=9"),
get_cache_key(&ctx, "GET", &header).primary_key_str()
);
assert_eq!(
"https://example.com/a?a=1",
ask("https://example.com/a?z=9&a=1", Some("a=1"))
);
}
#[test]
fn test_cache_key_alternatives() {
let list = |items: &[&str]| {
items
.iter()
.map(|item| item.to_string())
.collect::<Vec<_>>()
};
let mut ctx = Ctx::default();
assert_eq!(vec![list(&[])], ctx.cache_key_alternatives());
ctx.push_cache_key("fixed".to_string());
assert_eq!(vec![list(&["fixed"])], ctx.cache_key_alternatives());
ctx.push_cache_key_variant(
list(&["gzip"]),
Arc::new(vec![list(&[]), list(&["gzip"]), list(&["br"])]),
);
ctx.push_cache_key("lang".to_string());
ctx.push_cache_key_variant(
list(&[]),
Arc::new(vec![list(&[]), list(&["avif", "webp"])]),
);
assert_eq!(
Some(list(&["fixed", "gzip", "lang"])),
ctx.cache.as_ref().unwrap().keys
);
assert_eq!(
vec![
list(&["fixed", "lang"]),
list(&["fixed", "lang", "avif", "webp"]),
list(&["fixed", "gzip", "lang"]),
list(&["fixed", "gzip", "lang", "avif", "webp"]),
list(&["fixed", "br", "lang"]),
list(&["fixed", "br", "lang", "avif", "webp"]),
],
ctx.cache_key_alternatives()
);
let many: Vec<_> =
(0..40).map(|index| vec![index.to_string()]).collect();
let mut ctx = Ctx::default();
ctx.push_cache_key_variant(list(&["1"]), Arc::new(many.clone()));
ctx.push_cache_key_variant(list(&["2"]), Arc::new(many));
assert_eq!(vec![list(&["1", "2"])], ctx.cache_key_alternatives());
}
#[test]
fn test_ctx_new() {
let ctx = Ctx::new();
let elapsed_ms = ctx.timing.created_at.elapsed().as_millis();
assert!(elapsed_ms < 100, "created_at should be a recent timestamp");
assert!(ctx.cache.is_none());
assert!(ctx.features.is_none());
assert_eq!(ctx.conn.id, 0);
}
#[test]
fn test_tls_client_cert_log_fields() {
let mut ctx = Ctx::new();
let print = |ctx: &Ctx, key: &str| {
let mut buf = BytesMut::new();
ctx.append_log_value(&mut buf, key);
String::from_utf8_lossy(&buf).to_string()
};
assert_eq!("", print(&ctx, "tls_client_subject"));
assert_eq!("false", print(&ctx, "tls_client_verified"));
ctx.conn.tls_client_cert = Some(Arc::new(TlsClientCert {
subject: "O=Example, CN=device-42".to_string(),
fingerprint: "b545db7a".to_string(),
serial: "3429".to_string(),
}));
assert_eq!(
"O=Example, CN=device-42",
print(&ctx, "tls_client_subject")
);
assert_eq!("b545db7a", print(&ctx, "tls_client_fingerprint"));
assert_eq!("3429", print(&ctx, "tls_client_serial"));
assert_eq!("true", print(&ctx, "tls_client_verified"));
}
#[test]
fn test_plugin_values() {
let mut ctx = Ctx::new();
assert_eq!(None, ctx.get_plugin_value::<u16>("a"));
ctx.set_plugin_value("a", Arc::new(200u16));
ctx.set_plugin_value("b", "text".to_string());
assert_eq!(
Some(&200),
ctx.get_plugin_value::<Arc<u16>>("a").map(|v| &**v)
);
assert_eq!(
Some("text"),
ctx.get_plugin_value::<String>("b").map(|v| v.as_str())
);
assert_eq!(None, ctx.get_plugin_value::<u16>("a"));
ctx.set_plugin_value("a", 404u16);
assert_eq!(Some(&404), ctx.get_plugin_value::<u16>("a"));
}
#[test]
fn test_plugin_notes() {
let mut ctx = Ctx::new();
assert_eq!(None, ctx.get_plugin_note("a"));
ctx.set_plugin_note("a", "gzip");
ctx.set_plugin_note("b", "");
assert_eq!(Some("gzip"), ctx.get_plugin_note("a"));
assert_eq!(Some(""), ctx.get_plugin_note("b"));
assert_eq!(None, ctx.get_plugin_note("c"));
ctx.set_plugin_note("a", "br");
assert_eq!(Some("br"), ctx.get_plugin_note("a"));
assert_eq!(
2,
ctx.features
.as_ref()
.unwrap()
.plugin_notes
.as_ref()
.unwrap()
.len()
);
}
#[test]
fn test_add_and_get_variable() {
let mut ctx = Ctx::new();
assert!(
ctx.get_variable("key1").is_none(),
"Should be None before adding"
);
ctx.add_variable("key1", "value1");
ctx.add_variable("key2", "value2");
assert_eq!(ctx.get_variable("key1"), Some("value1"));
assert_eq!(ctx.get_variable("key2"), Some("value2"));
assert_eq!(ctx.get_variable("nonexistent"), None);
}
#[test]
fn test_get_time_field() {
let mut ctx = Ctx::new();
ctx.timing.upstream_response = Some(100);
assert_eq!(ctx.get_upstream_response_time(), Some(100));
ctx.timing.upstream_response = Some(-1);
assert_eq!(
ctx.get_upstream_response_time(),
None,
"Time exceeding one hour should be None"
);
ctx.timing.upstream_response = None;
assert_eq!(ctx.get_upstream_response_time(), None);
}
#[test]
fn test_ctx_log_field_names() {
for (name, field) in [
("connection_id", CtxLogField::ConnectionId),
(
"upstream_tcp_connect_time_human",
CtxLogField::UpstreamTcpConnectTimeHuman,
),
(
"upstream_connect_offload_wait_time",
CtxLogField::UpstreamConnectOffloadWaitTime,
),
("tls_version", CtxLogField::TlsVersion),
("tls_client_subject", CtxLogField::TlsClientSubject),
("tls_client_fingerprint", CtxLogField::TlsClientFingerprint),
("tls_client_serial", CtxLogField::TlsClientSerial),
("tls_client_verified", CtxLogField::TlsClientVerified),
("ja4", CtxLogField::Ja4),
("ja4_r", CtxLogField::Ja4R),
("ja4_o", CtxLogField::Ja4O),
("ja4_ro", CtxLogField::Ja4Ro),
("compression_ratio", CtxLogField::CompressionRatio),
("service_time_human", CtxLogField::ServiceTimeHuman),
] {
assert_eq!(Ok(field), name.parse::<CtxLogField>(), "{name}");
assert_eq!(name, field.to_string());
}
assert_eq!(true, "unknown_key".parse::<CtxLogField>().is_err());
let mut ctx = Ctx::new();
ctx.conn.id = 7;
let mut by_name = BytesMut::new();
ctx.append_log_value(&mut by_name, "connection_id");
let mut by_field = BytesMut::new();
ctx.append_log_field(&mut by_field, CtxLogField::ConnectionId);
assert_eq!(b"7", by_name.as_ref());
assert_eq!(by_name, by_field);
}
#[test]
fn test_ja4_log_fields() {
let mut ctx = Ctx::new();
let value = |ctx: &Ctx, key: &str| {
let mut buf = BytesMut::new();
ctx.append_log_value(&mut buf, key);
String::from_utf8(buf.to_vec()).unwrap()
};
assert_eq!("", value(&ctx, "ja4"));
let fingerprint = Ja4Fingerprint::from_client_hello(
&crate::ja4::testing::spec_example_body(),
)
.unwrap();
ctx.conn.ja4 = Some(Arc::new(fingerprint.clone()));
assert_eq!("t13d1516h2_8daaf6152771_e5627efa2ab1", value(&ctx, "ja4"));
assert_eq!(fingerprint.ja4_r(), value(&ctx, "ja4_r"));
assert_eq!(fingerprint.ja4_o(), value(&ctx, "ja4_o"));
assert_eq!(fingerprint.ja4_ro(), value(&ctx, "ja4_ro"));
}
#[test]
fn test_append_log_value_coverage() {
let mut ctx = Ctx::new();
let mut buf = BytesMut::new();
ctx.append_log_value(&mut buf, "unknown_key");
assert!(buf.is_empty(), "Unknown key should not append anything");
buf = BytesMut::new();
ctx.conn.reused = true;
ctx.append_log_value(&mut buf, "connection_reused");
assert_eq!(&buf[..], b"true");
ctx.conn.tls_version = Some("TLSv1.3".into());
buf = BytesMut::new();
ctx.append_log_value(&mut buf, "tls_version");
assert_eq!(&buf[..], b"TLSv1.3");
std::thread::sleep(Duration::from_millis(11));
buf = BytesMut::new();
ctx.append_log_value(&mut buf, "service_time");
let service_time: u64 =
std::str::from_utf8(&buf[..]).unwrap().parse().unwrap();
assert!(service_time >= 10, "Service time should be at least 10ms");
}
#[test]
fn test_get_cache_key() {
let method = "GET";
let mut h2 = RequestHeader::build("GET", b"/", None).unwrap();
h2.set_uri(Uri::from_static("https://example.com/path?a=1"));
let h1 = |host: &str, path: &str| {
let mut header =
RequestHeader::build("GET", path.as_bytes(), None).unwrap();
header.insert_header("Host", host).unwrap();
header
};
let ctx_no_cache = Ctx::new();
let key1 = get_cache_key(&ctx_no_cache, method, &h2);
assert_eq!(key1.user_tag, "");
assert_eq!(key1.primary_key_str(), Some(""));
let mut ctx_with_ns = Ctx::new();
ctx_with_ns.cache = Some(Box::new(CacheInfo {
namespace: Some("my-ns".to_string()),
..Default::default()
}));
let key2 = get_cache_key(&ctx_with_ns, method, &h2);
assert_eq!(key2.user_tag, "my-ns");
assert_eq!(
key2.primary_key_str(),
Some("my-nsGET:example.com/path?a=1")
);
assert_eq!(key2.primary(), "8fe845e5498004cd81203be9074a8e68");
for host in ["example.com", "Example.COM", "example.com:8443"] {
let key =
get_cache_key(&ctx_with_ns, method, &h1(host, "/path?a=1"));
assert_eq!(key2.primary(), key.primary(), "{host}");
}
let other =
get_cache_key(&ctx_with_ns, method, &h1("other.com", "/path?a=1"));
assert_eq!(
other.primary_key_str(),
Some("my-nsGET:other.com/path?a=1")
);
assert_ne!(key2.primary(), other.primary());
let odd = get_cache_key(
&ctx_with_ns,
method,
&h1("example.com/static", "/app.js"),
);
let plain = get_cache_key(
&ctx_with_ns,
method,
&h1("example.com", "/static/app.js"),
);
assert_eq!(
odd.primary_key_str(),
Some("my-nsGET:example.com%2Fstatic/app.js")
);
assert_ne!(odd.primary(), plain.primary());
let escaped = get_cache_key(
&ctx_with_ns,
method,
&h1("example.com%2Fstatic", "/app.js"),
);
assert_ne!(odd.primary(), escaped.primary());
let bare = RequestHeader::build("GET", b"/path", None).unwrap();
assert_eq!(
get_cache_key(&ctx_with_ns, method, &bare).primary_key_str(),
Some("my-nsGET:/path")
);
let mut ctx_with_keys = Ctx::new();
ctx_with_keys.cache = Some(Box::new(CacheInfo {
namespace: Some("my-ns".to_string()),
keys: Some(vec!["user-123".to_string(), "desktop".to_string()]),
..Default::default()
}));
let key3 = get_cache_key(&ctx_with_keys, method, &h2);
assert_eq!(key3.user_tag, "my-ns");
assert_eq!(
key3.primary_key_str(),
Some("my-nsuser-123:desktop:GET:example.com/path?a=1")
);
}
#[test]
fn test_generate_server_timing() {
let mut ctx = Ctx::new();
ctx.timing.upstream_connect = Some(1);
ctx.timing.upstream_processing = Some(2);
ctx.timing.cache_lookup = Some(6);
ctx.timing.cache_lock = Some(7);
ctx.add_plugin_processing_time(&Arc::from("plugin1"), 100);
let timing_header = ctx.generate_server_timing();
assert!(timing_header.contains("upstream.connect;dur=1"));
assert!(timing_header.contains("upstream.processing;dur=2"));
assert!(timing_header.contains("upstream;dur=3"));
assert!(timing_header.contains("cache.lookup;dur=6"));
assert!(timing_header.contains("cache.lock;dur=7"));
assert!(timing_header.contains("cache;dur=13"));
assert!(timing_header.contains("plugin.plugin1;dur=100"));
assert!(timing_header.contains("plugin;dur=100"));
assert!(timing_header.contains("total;dur="));
}
#[test]
fn test_format_duration() {
let mut buf = BytesMut::new();
format_duration(&mut buf, (3600 + 3500) * 1000);
assert_eq!(b"1.9h", buf.as_ref());
buf = BytesMut::new();
format_duration(&mut buf, (3600 + 1800) * 1000);
assert_eq!(b"1.5h", buf.as_ref());
buf = BytesMut::new();
format_duration(&mut buf, (3600 + 100) * 1000);
assert_eq!(b"1h", buf.as_ref());
buf = BytesMut::new();
format_duration(&mut buf, (60 + 50) * 1000);
assert_eq!(b"1.8m", buf.as_ref());
buf = BytesMut::new();
format_duration(&mut buf, (60 + 2) * 1000);
assert_eq!(b"1m", buf.as_ref());
buf = BytesMut::new();
format_duration(&mut buf, 1000);
assert_eq!(b"1s", buf.as_ref());
buf = BytesMut::new();
format_duration(&mut buf, 512);
assert_eq!(b"512ms", buf.as_ref());
buf = BytesMut::new();
format_duration(&mut buf, 1112);
assert_eq!(b"1.1s", buf.as_ref());
}
#[test]
fn test_add_variable() {
let mut ctx = Ctx::new();
ctx.add_variable("key1", "value1");
ctx.add_variable("key2", "value2");
ctx.extend_variables(AHashMap::from([
("key3".to_string(), "value3".to_string()),
("key4".to_string(), "value4".to_string()),
]));
let variables =
ctx.features.as_ref().unwrap().variables.as_ref().unwrap();
assert_eq!(variables.get("key1"), Some(&"value1".to_string()));
assert_eq!(variables.get("key2"), Some(&"value2".to_string()));
assert_eq!(variables.get("key3"), Some(&"value3".to_string()));
assert_eq!(variables.get("key4"), Some(&"value4".to_string()));
}
#[test]
fn test_cache_key() {
let mut ctx = Ctx::new();
ctx.push_cache_key("key1".to_string());
ctx.extend_cache_keys(vec!["key2".to_string(), "key3".to_string()]);
assert_eq!(
vec!["key1".to_string(), "key2".to_string(), "key3".to_string()],
ctx.cache.unwrap().keys.unwrap()
);
let mut ctx = Ctx::new();
ctx.cache.get_or_insert_default();
let mut header = RequestHeader::build("GET", b"/", None).unwrap();
header.set_uri(Uri::from_static("https://example.com/path"));
let key = get_cache_key(&ctx, "GET", &header);
assert_eq!(key.user_tag, "");
assert_eq!(key.primary_key_str(), Some("GET:example.com/path"));
}
#[test]
fn test_state() {
let mut ctx = Ctx::new();
let mut buf = BytesMut::new();
ctx.conn.id = 10;
ctx.append_log_value(&mut buf, "connection_id");
assert_eq!(b"10", buf.as_ref());
buf = BytesMut::new();
ctx.append_log_value(&mut buf, "upstream_reused");
assert_eq!(b"false", buf.as_ref());
buf = BytesMut::new();
ctx.upstream.reused = true;
ctx.append_log_value(&mut buf, "upstream_reused");
assert_eq!(b"true", buf.as_ref());
buf = BytesMut::new();
ctx.upstream.address = "192.168.1.1:80".to_string();
ctx.append_log_value(&mut buf, "upstream_addr");
assert_eq!(b"192.168.1.1:80", buf.as_ref());
buf = BytesMut::new();
ctx.upstream.name = "standby".into();
ctx.append_log_value(&mut buf, "upstream_name");
assert_eq!(b"standby", buf.as_ref());
buf = BytesMut::new();
ctx.upstream.status = Some(StatusCode::CREATED);
ctx.append_log_value(&mut buf, "upstream_status");
assert_eq!(b"201", buf.as_ref());
buf = BytesMut::new();
ctx.state.processing_count = 10;
ctx.append_log_value(&mut buf, "processing");
assert_eq!(b"10", buf.as_ref());
buf = BytesMut::new();
ctx.timing.upstream_connect = Some(1);
ctx.append_log_value(&mut buf, "upstream_connect_time");
assert_eq!(b"1", buf.as_ref());
buf = BytesMut::new();
ctx.append_log_value(&mut buf, "upstream_connect_time_human");
assert_eq!(b"1ms", buf.as_ref());
buf = BytesMut::new();
ctx.upstream.connected_count = Some(30);
ctx.append_log_value(&mut buf, "upstream_connected");
assert_eq!(b"30", buf.as_ref());
buf = BytesMut::new();
ctx.timing.upstream_processing = Some(2);
ctx.append_log_value(&mut buf, "upstream_processing_time");
assert_eq!(b"2", buf.as_ref());
buf = BytesMut::new();
ctx.append_log_value(&mut buf, "upstream_processing_time_human");
assert_eq!(b"2ms", buf.as_ref());
buf = BytesMut::new();
ctx.timing.upstream_response = Some(3);
ctx.append_log_value(&mut buf, "upstream_response_time");
assert_eq!(b"3", buf.as_ref());
buf = BytesMut::new();
ctx.append_log_value(&mut buf, "upstream_response_time_human");
assert_eq!(b"3ms", buf.as_ref());
buf = BytesMut::new();
ctx.append_log_value(&mut buf, "upstream_first_body_time");
assert_eq!(b"", buf.as_ref());
buf = BytesMut::new();
ctx.timing.upstream_first_body = Some(-5);
ctx.append_log_value(&mut buf, "upstream_first_body_time");
assert_eq!(b"", buf.as_ref());
buf = BytesMut::new();
ctx.timing.upstream_first_body = Some(1250);
ctx.append_log_value(&mut buf, "upstream_first_body_time");
assert_eq!(b"1250", buf.as_ref());
buf = BytesMut::new();
ctx.append_log_value(&mut buf, "upstream_first_body_time_human");
assert_eq!(b"1.2s", buf.as_ref());
buf = BytesMut::new();
ctx.timing.upstream_tcp_connect = Some(100);
ctx.append_log_value(&mut buf, "upstream_tcp_connect_time");
assert_eq!(b"100", buf.as_ref());
buf = BytesMut::new();
ctx.append_log_value(&mut buf, "upstream_tcp_connect_time_human");
assert_eq!(b"100ms", buf.as_ref());
buf = BytesMut::new();
ctx.timing.upstream_tls_handshake = Some(110);
ctx.append_log_value(&mut buf, "upstream_tls_handshake_time");
assert_eq!(b"110", buf.as_ref());
buf = BytesMut::new();
ctx.timing.upstream_connect_offload_wait = Some(3);
ctx.append_log_value(&mut buf, "upstream_connect_offload_wait_time");
assert_eq!(b"3", buf.as_ref());
buf = BytesMut::new();
ctx.append_log_value(
&mut buf,
"upstream_connect_offload_wait_time_human",
);
assert_eq!(b"3ms", buf.as_ref());
buf = BytesMut::new();
ctx.append_log_value(&mut buf, "upstream_tls_handshake_time_human");
assert_eq!(b"110ms", buf.as_ref());
buf = BytesMut::new();
ctx.timing.upstream_connection_duration = Some(120);
ctx.append_log_value(&mut buf, "upstream_connection_time");
assert_eq!(b"120", buf.as_ref());
buf = BytesMut::new();
ctx.append_log_value(&mut buf, "upstream_connection_time_human");
assert_eq!(b"120ms", buf.as_ref());
buf = BytesMut::new();
ctx.upstream.location = "pingap".to_string().into();
ctx.append_log_value(&mut buf, "location");
assert_eq!(b"pingap", buf.as_ref());
buf = BytesMut::new();
ctx.timing.connection_duration = 4;
ctx.append_log_value(&mut buf, "connection_time");
assert_eq!(b"4", buf.as_ref());
buf = BytesMut::new();
ctx.append_log_value(&mut buf, "connection_time_human");
assert_eq!(b"4ms", buf.as_ref());
buf = BytesMut::new();
ctx.conn.reused = false;
ctx.append_log_value(&mut buf, "connection_reused");
assert_eq!(b"false", buf.as_ref());
buf = BytesMut::new();
ctx.conn.reused = true;
ctx.append_log_value(&mut buf, "connection_reused");
assert_eq!(b"true", buf.as_ref());
buf = BytesMut::new();
ctx.conn.tls_version = Some("TLSv1.3".into());
ctx.append_log_value(&mut buf, "tls_version");
assert_eq!(b"TLSv1.3", buf.as_ref());
buf = BytesMut::new();
ctx.conn.tls_cipher =
Some("ECDHE_ECDSA_WITH_AES_128_GCM_SHA256".into());
ctx.append_log_value(&mut buf, "tls_cipher");
assert_eq!(b"ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", buf.as_ref());
buf = BytesMut::new();
ctx.timing.tls_handshake = Some(101);
ctx.append_log_value(&mut buf, "tls_handshake_time");
assert_eq!(b"101", buf.as_ref());
buf = BytesMut::new();
ctx.append_log_value(&mut buf, "tls_handshake_time_human");
assert_eq!(b"101ms", buf.as_ref());
{
let features = ctx.features.get_or_insert_default();
features.compression_stat = Some(CompressionStat {
in_bytes: 1024,
out_bytes: 500,
duration: Duration::from_millis(5),
..Default::default()
})
}
buf = BytesMut::new();
ctx.append_log_value(&mut buf, "compression_time");
assert_eq!(b"5", buf.as_ref());
buf = BytesMut::new();
ctx.append_log_value(&mut buf, "compression_time_human");
assert_eq!(b"5ms", buf.as_ref());
buf = BytesMut::new();
ctx.append_log_value(&mut buf, "compression_ratio");
assert_eq!(b"2.0", buf.as_ref());
buf = BytesMut::new();
ctx.timing.cache_lookup = Some(6);
ctx.append_log_value(&mut buf, "cache_lookup_time");
assert_eq!(b"6", buf.as_ref());
buf = BytesMut::new();
ctx.append_log_value(&mut buf, "cache_lookup_time_human");
assert_eq!(b"6ms", buf.as_ref());
buf = BytesMut::new();
ctx.timing.cache_lock = Some(7);
ctx.append_log_value(&mut buf, "cache_lock_time");
assert_eq!(b"7", buf.as_ref());
buf = BytesMut::new();
ctx.append_log_value(&mut buf, "cache_lock_time_human");
assert_eq!(b"7ms", buf.as_ref());
}
#[test]
fn test_add_plugin_processing_time() {
let mut ctx = Ctx::new();
ctx.add_plugin_processing_time(&Arc::from("plugin1"), 100);
ctx.add_plugin_processing_time(&Arc::from("plugin2"), 200);
assert_eq!(
ctx.features.unwrap().plugin_processing_times,
Some(vec![
(Arc::from("plugin1"), 100),
(Arc::from("plugin2"), 200)
])
);
}
#[test]
fn test_get_digest_detail() {
let mut digest = Digest::default();
let detail = get_digest_detail(&digest);
assert_eq!(detail.connection_reused, false);
assert_eq!(detail.connection_time, 0);
assert_eq!(detail.tcp_established, 0);
assert_eq!(detail.tls_established, 0);
assert_eq!(detail.tls_version, None);
assert_eq!(detail.tls_cipher, None);
digest.timing_digest.push(Some(TimingDigest {
established_ts: SystemTime::UNIX_EPOCH
.checked_add(Duration::from_secs(5))
.unwrap(),
..Default::default()
}));
digest.timing_digest.push(Some(TimingDigest {
established_ts: SystemTime::UNIX_EPOCH
.checked_add(Duration::from_secs(3))
.unwrap(),
..Default::default()
}));
digest.ssl_digest = Some(Arc::new(SslDigest {
version: "1.3".into(),
cipher: "123".into(),
organization: Some("cloudflare".to_string()),
serial_number: Some(
"0x00000000000000000000000000000abc".to_string(),
),
cert_digest: vec![],
extension: SslDigestExtension::default(),
}));
let detail = get_digest_detail(&digest);
assert_eq!(detail.connection_reused, true);
assert_eq!(detail.tcp_established, 5000);
assert_eq!(detail.tls_established, 3000);
assert_eq!(detail.tls_version.as_deref(), Some("1.3"));
assert_eq!(detail.tls_cipher.as_deref(), Some("123"));
assert_eq!(detail.tcp_connect, None);
assert_eq!(detail.tls_handshake, None);
digest.timing_digest = vec![
Some(TimingDigest {
establishment_duration: Some(Duration::from_millis(12)),
..Default::default()
}),
Some(TimingDigest {
establishment_duration: Some(Duration::from_millis(34)),
..Default::default()
}),
];
let detail = get_digest_detail(&digest);
assert_eq!(detail.tcp_connect, Some(12));
assert_eq!(detail.tls_handshake, Some(34));
assert_eq!(detail.connect_offload_wait, None);
digest.timing_digest[0] = Some(TimingDigest {
establishment_duration: Some(Duration::from_millis(12)),
offload_wait_duration: Some(Duration::from_millis(2)),
..Default::default()
});
let detail = get_digest_detail(&digest);
assert_eq!(detail.connect_offload_wait, Some(2));
digest.ssl_digest = None;
digest.timing_digest.truncate(1);
let detail = get_digest_detail(&digest);
assert_eq!(detail.tcp_connect, Some(12));
assert_eq!(detail.tls_handshake, None);
}
#[test]
fn test_update_upstream_timing_from_digest() {
let measured = |tcp: u64, tls: u64| Digest {
timing_digest: vec![
Some(TimingDigest {
establishment_duration: Some(Duration::from_millis(tcp)),
..Default::default()
}),
Some(TimingDigest {
establishment_duration: Some(Duration::from_millis(tls)),
..Default::default()
}),
],
ssl_digest: Some(Arc::new(SslDigest {
version: "1.3".into(),
cipher: "123".into(),
organization: None,
serial_number: None,
cert_digest: vec![],
extension: SslDigestExtension::default(),
})),
..Default::default()
};
let mut ctx = Ctx::new();
ctx.timing.upstream_connect = Some(100);
ctx.update_upstream_timing_from_digest(&measured(12, 34), false);
assert_eq!(Some(12), ctx.timing.upstream_tcp_connect);
assert_eq!(Some(34), ctx.timing.upstream_tls_handshake);
assert_eq!(None, ctx.timing.upstream_connect_offload_wait);
let mut ctx = Ctx::new();
let mut offloaded = measured(12, 34);
offloaded.timing_digest[0] = Some(TimingDigest {
establishment_duration: Some(Duration::from_millis(12)),
offload_wait_duration: Some(Duration::from_millis(2)),
..Default::default()
});
ctx.update_upstream_timing_from_digest(&offloaded, false);
assert_eq!(Some(2), ctx.timing.upstream_connect_offload_wait);
let mut ctx = Ctx::new();
ctx.timing.upstream_connect = Some(100);
ctx.update_upstream_timing_from_digest(&measured(12, 34), true);
assert_eq!(None, ctx.timing.upstream_tcp_connect);
assert_eq!(None, ctx.timing.upstream_tls_handshake);
let mut ctx = Ctx::new();
ctx.timing.upstream_connect = Some(100);
let mut unmeasured = measured(0, 0);
unmeasured.timing_digest = vec![
Some(TimingDigest {
established_ts: SystemTime::UNIX_EPOCH
.checked_add(Duration::from_millis(1_000))
.unwrap(),
..Default::default()
}),
Some(TimingDigest {
established_ts: SystemTime::UNIX_EPOCH
.checked_add(Duration::from_millis(1_030))
.unwrap(),
..Default::default()
}),
];
ctx.update_upstream_timing_from_digest(&unmeasured, false);
assert_eq!(Some(70), ctx.timing.upstream_tcp_connect);
assert_eq!(Some(30), ctx.timing.upstream_tls_handshake);
}
#[test]
fn test_modify_body_handler() {
let mut ctx = Ctx::default();
struct TestHandler {}
impl ModifyResponseBody for TestHandler {
fn handle(
&mut self,
_session: &Session,
body: &mut Option<bytes::Bytes>,
_end_of_stream: bool,
) -> pingora::Result<()> {
*body = Some(Bytes::from("test"));
Ok(())
}
}
ctx.add_modify_body_handler("test", Box::new(TestHandler {}));
assert_eq!(true, ctx.get_modify_body_handler("test").is_some());
}
}