Skip to main content

powhttp_sdk/
sessions.rs

1use std::net::IpAddr;
2use std::ops::Deref;
3use std::time::Duration;
4use serde::{Serialize, Deserialize};
5use ulid::Ulid;
6use url::Url;
7use chrono::{DateTime, Utc};
8use crate::shared::NamedU16;
9use crate::proxy_server::ProcessInfo;
10use crate::serde_helpers::option_duration_millis;
11
12/// A single HTTP transaction captured by the proxy.
13///
14/// Contains the full request/response pair, timing information, TLS details
15/// and optional HTTP/2 stream reference.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct SessionEntry {
19    pub id: Ulid,
20    pub url: Url,
21    pub client_addr: Option<NetAddress>,
22    pub remote_addr: Option<NetAddress>,
23    pub http_version: String,
24    pub transaction_type: TransactionType,
25    pub request: Request,
26    pub response: Option<Response>,
27    #[serde(rename = "isWebSocket")]
28    pub is_websocket: bool,
29    pub tls: TlsSettings,
30    pub http2: Option<Http2StreamRef>,
31    pub timings: Timings,
32    pub process: Option<ProcessInfo>,
33}
34
35/// An IP address with an optional port.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct NetAddress {
39    pub ip: IpAddr,
40    pub port: Option<u16>,
41}
42
43/// Whether the entry represents a regular request or an HTTP/2 server push.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub enum TransactionType {
47    Request,
48    PushPromise,
49}
50
51/// TLS connection metadata associated with a session entry.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53#[serde(rename_all = "camelCase")]
54pub struct TlsSettings {
55    pub connection_id: Option<Ulid>,
56    pub tls_version: Option<NamedU16>,
57    pub cipher_suite: Option<NamedU16>,
58    pub ja3: Option<Ja3>,
59    pub ja4: Option<Ja4>,
60}
61
62/// Reference to an HTTP/2 stream within a connection.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub struct Http2StreamRef {
66    pub connection_id: Ulid,
67    pub stream_id: u32,
68}
69
70/// Timing breakdown for a session entry.
71///
72/// Each phase is optional and will be `None` when the phase was not
73/// observed or does not apply.
74#[derive(Debug, Clone, Serialize, Deserialize)]
75#[serde(rename_all = "camelCase")]
76pub struct Timings {
77    /// Request start (UTC).
78    pub started_at: DateTime<Utc>,
79    /// Duration in milliseconds.
80    #[serde(with = "option_duration_millis")]
81    pub blocked: Option<Duration>,
82    /// Duration in milliseconds.
83    #[serde(with = "option_duration_millis")]
84    pub dns: Option<Duration>,
85    /// Duration in milliseconds.
86    #[serde(with = "option_duration_millis")]
87    pub connect: Option<Duration>,
88    /// Duration in milliseconds.
89    #[serde(with = "option_duration_millis")]
90    pub send: Option<Duration>,
91    /// Duration in milliseconds.
92    #[serde(with = "option_duration_millis")]
93    pub wait: Option<Duration>,
94    /// Duration in milliseconds.
95    #[serde(with = "option_duration_millis")]
96    pub receive: Option<Duration>,
97    /// Duration in milliseconds.
98    #[serde(with = "option_duration_millis")]
99    pub ssl: Option<Duration>,
100}
101
102/// JA3 TLS fingerprint (raw string + MD5 hash).
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104#[serde(rename_all = "camelCase")]
105pub struct Ja3 {
106    pub string: String,
107    pub hash: String,
108}
109
110/// JA4 TLS fingerprint (raw + hashed form).
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
112pub struct Ja4 {
113    pub raw: String,
114    pub hashed: String,
115}
116
117/// Ordered list of HTTP headers.
118///
119/// Header name lookups are case-insensitive. Duplicate names are allowed,
120/// use [`get_all`](Headers::get_all) to retrieve every value for a given name.
121///
122/// ```
123/// use powhttp_sdk::Headers;
124///
125/// let mut headers = Headers::new();
126/// headers.push("Content-Type", "application/json");
127/// headers.push("Accept", "text/html");
128///
129/// assert_eq!(headers.get("content-type"), Some("application/json"));
130/// assert_eq!(headers.len(), 2);
131/// ```
132#[derive(Debug, Clone, Default, PartialEq, Eq)]
133pub struct Headers {
134    inner: Vec<(String, String)>,
135}
136
137impl Headers {
138    /// Creates an empty header list.
139    pub fn new() -> Self {
140        Self { inner: Vec::new() }
141    }
142
143    /// Returns the value of the first header matching `name` (case-insensitive).
144    pub fn get(&self, name: &str) -> Option<&str> {
145        let name_lower = name.to_lowercase();
146        self.inner
147            .iter()
148            .find(|(name, _)| name.to_lowercase() == name_lower)
149            .map(|(_, value)| value.as_str())
150    }
151
152    /// Returns all values for headers matching `name` (case-insensitive).
153    pub fn get_all(&self, name: &str) -> Vec<&str> {
154        let name_lower = name.to_lowercase();
155        self.inner
156            .iter()
157            .filter(|(name, _)| name.to_lowercase() == name_lower)
158            .map(|(_, value)| value.as_str())
159            .collect()
160    }
161
162    /// Returns `true` if a header with the given `name` exists (case-insensitive).
163    pub fn contains(&self, name: &str) -> bool {
164        let name_lower = name.to_lowercase();
165        self.inner.iter().any(|(name, _)| name.to_lowercase() == name_lower)
166    }
167
168    /// Sets the value of the first header matching `name` or appends a new one.
169    pub fn set(&mut self, name: impl Into<String>, value: impl Into<String>) {
170        let name = name.into();
171        let name_lower = name.to_lowercase();
172        if let Some((_, v)) = self.inner.iter_mut().find(|(name, _)| name.to_lowercase() == name_lower) {
173            *v = value.into();
174        } else {
175            self.inner.push((name, value.into()));
176        }
177    }
178
179    /// Appends a header even if one with the same name already exists.
180    pub fn push(&mut self, name: impl Into<String>, value: impl Into<String>) {
181        self.inner.push((name.into(), value.into()));
182    }
183
184    /// Removes and returns the value of the first header matching `name`.
185    pub fn remove(&mut self, name: &str) -> Option<String> {
186        let name_lower = name.to_lowercase();
187        if let Some(pos) = self.inner.iter().position(|(name, _)| name.to_lowercase() == name_lower) {
188            Some(self.inner.remove(pos).1)
189        } else {
190            None
191        }
192    }
193
194    pub fn iter(&self) -> std::slice::Iter<'_, (String, String)> {
195        self.inner.iter()
196    }
197
198    pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, (String, String)> {
199        self.inner.iter_mut()
200    }
201
202    pub fn len(&self) -> usize {
203        self.inner.len()
204    }
205
206    pub fn is_empty(&self) -> bool {
207        self.inner.is_empty()
208    }
209}
210
211impl Deref for Headers {
212    type Target = [(String, String)];
213
214    fn deref(&self) -> &Self::Target {
215        &self.inner
216    }
217}
218
219impl IntoIterator for Headers {
220    type Item = (String, String);
221    type IntoIter = std::vec::IntoIter<(String, String)>;
222
223    fn into_iter(self) -> Self::IntoIter {
224        self.inner.into_iter()
225    }
226}
227
228impl<'a> IntoIterator for &'a Headers {
229    type Item = &'a (String, String);
230    type IntoIter = std::slice::Iter<'a, (String, String)>;
231
232    fn into_iter(self) -> Self::IntoIter {
233        self.inner.iter()
234    }
235}
236
237impl<'a> IntoIterator for &'a mut Headers {
238    type Item = &'a mut (String, String);
239    type IntoIter = std::slice::IterMut<'a, (String, String)>;
240
241    fn into_iter(self) -> Self::IntoIter {
242        self.inner.iter_mut()
243    }
244}
245
246impl FromIterator<(String, String)> for Headers {
247    fn from_iter<T: IntoIterator<Item = (String, String)>>(iter: T) -> Self {
248        Self { 
249            inner: Vec::from_iter(iter)
250        }
251    }
252}
253
254impl From<Vec<(String, String)>> for Headers {
255    fn from(vec: Vec<(String, String)>) -> Self {
256        Self { inner: vec }
257    }
258}
259
260impl From<Headers> for Vec<(String, String)> {
261    fn from(headers: Headers) -> Self {
262        headers.inner
263    }
264}
265
266impl Serialize for Headers {
267    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
268        self.inner.serialize(serializer)
269    }
270}
271
272impl<'de> Deserialize<'de> for Headers {
273    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
274        Vec::<(String, String)>::deserialize(deserializer).map(|inner| Headers { inner })
275    }
276}
277
278/// The request half of a session entry.
279#[derive(Debug, Clone, Serialize, Deserialize)]
280#[serde(rename_all = "camelCase")]
281pub struct Request {
282    pub method: Option<String>,
283    pub path: Option<String>,
284    pub http_version: Option<String>,
285    pub headers: Headers,
286    pub body_size: Option<usize>,
287}
288
289/// The response half of a session entry.
290#[derive(Debug, Clone, Serialize, Deserialize)]
291#[serde(rename_all = "camelCase")]
292pub struct Response {
293    pub http_version: Option<String>,
294    pub status_code: Option<u16>,
295    pub status_text: Option<String>,
296    pub headers: Headers,
297    pub body_size: Option<usize>,
298}
299
300/// Lightweight session descriptor returned by [`ExtensionHandle::list_sessions`](crate::ExtensionHandle::list_sessions).
301#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
302#[serde(rename_all = "camelCase")]
303pub struct SessionInfo {
304    pub id: Ulid,
305    pub name: String,
306}