use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant;
use http::{HeaderMap, HeaderValue, Method, StatusCode, Uri, Version, header::HeaderName};
use parking_lot::{Mutex, RwLock};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct InterceptId(pub u64);
impl std::fmt::Display for InterceptId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "intercept-{}", self.0)
}
}
#[derive(Debug, Clone)]
pub struct InterceptConfig {
pub intercept_requests: bool,
pub intercept_responses: bool,
pub max_body_size: usize,
pub url_patterns: Vec<String>,
pub methods: Vec<Method>,
pub intercept_tls: bool,
pub log_traffic: bool,
}
impl Default for InterceptConfig {
fn default() -> Self {
Self {
intercept_requests: true,
intercept_responses: true,
max_body_size: 10 * 1024 * 1024, url_patterns: Vec::new(),
methods: Vec::new(),
intercept_tls: true,
log_traffic: false,
}
}
}
impl InterceptConfig {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_request_intercept(mut self, enabled: bool) -> Self {
self.intercept_requests = enabled;
self
}
#[must_use]
pub fn with_response_intercept(mut self, enabled: bool) -> Self {
self.intercept_responses = enabled;
self
}
#[must_use]
pub fn with_url_pattern(mut self, pattern: &str) -> Self {
self.url_patterns.push(pattern.to_string());
self
}
#[must_use]
pub fn with_method(mut self, method: Method) -> Self {
self.methods.push(method);
self
}
pub fn passive() -> Self {
Self {
intercept_requests: false,
intercept_responses: false,
max_body_size: 10 * 1024 * 1024,
url_patterns: Vec::new(),
methods: Vec::new(),
intercept_tls: false,
log_traffic: true,
}
}
pub fn active() -> Self {
Self {
intercept_requests: true,
intercept_responses: true,
max_body_size: 50 * 1024 * 1024,
url_patterns: Vec::new(),
methods: Vec::new(),
intercept_tls: true,
log_traffic: true,
}
}
}
#[derive(Debug, Clone)]
pub struct InterceptedRequest {
pub id: InterceptId,
pub method: Method,
pub uri: Uri,
pub version: Version,
pub headers: HeaderMap,
pub body: Vec<u8>,
pub timestamp: Instant,
pub client_addr: Option<String>,
pub is_tls: bool,
}
impl InterceptedRequest {
pub fn new(id: InterceptId, method: Method, uri: Uri) -> Self {
Self {
id,
method,
uri,
version: Version::HTTP_11,
headers: HeaderMap::new(),
body: Vec::new(),
timestamp: Instant::now(),
client_addr: None,
is_tls: false,
}
}
#[must_use]
pub fn with_version(mut self, version: Version) -> Self {
self.version = version;
self
}
#[must_use]
pub fn with_headers(mut self, headers: HeaderMap) -> Self {
self.headers = headers;
self
}
#[must_use]
pub fn with_body(mut self, body: Vec<u8>) -> Self {
self.body = body;
self
}
pub fn header(&self, name: &str) -> Option<&str> {
self.headers.get(name).and_then(|v| v.to_str().ok())
}
pub fn body_text(&self) -> Option<String> {
String::from_utf8(self.body.clone()).ok()
}
pub fn content_type(&self) -> Option<&str> {
self.header("content-type")
}
pub fn is_json(&self) -> bool {
self.content_type()
.map(|ct| ct.contains("application/json"))
.unwrap_or(false)
}
pub fn host(&self) -> Option<String> {
self.uri
.host()
.map(String::from)
.or_else(|| self.header("host").map(String::from))
}
}
#[derive(Debug, Clone)]
pub struct InterceptedResponse {
pub request_id: InterceptId,
pub status: StatusCode,
pub version: Version,
pub headers: HeaderMap,
pub body: Vec<u8>,
pub timestamp: Instant,
pub response_time: std::time::Duration,
}
impl InterceptedResponse {
pub fn new(
request_id: InterceptId,
status: StatusCode,
response_time: std::time::Duration,
) -> Self {
Self {
request_id,
status,
version: Version::HTTP_11,
headers: HeaderMap::new(),
body: Vec::new(),
timestamp: Instant::now(),
response_time,
}
}
#[must_use]
pub fn with_headers(mut self, headers: HeaderMap) -> Self {
self.headers = headers;
self
}
#[must_use]
pub fn with_body(mut self, body: Vec<u8>) -> Self {
self.body = body;
self
}
pub fn header(&self, name: &str) -> Option<&str> {
self.headers.get(name).and_then(|v| v.to_str().ok())
}
pub fn body_text(&self) -> Option<String> {
String::from_utf8(self.body.clone()).ok()
}
pub fn is_success(&self) -> bool {
self.status.is_success()
}
}
#[derive(Debug, Clone)]
pub enum RequestAction {
Forward,
Modify(ModifiedRequest),
Drop,
Respond(InterceptedResponse),
}
#[derive(Debug, Clone)]
pub enum ResponseAction {
Forward,
Modify(ModifiedResponse),
Drop,
}
#[derive(Debug, Clone)]
pub struct ModifiedRequest {
pub method: Option<Method>,
pub uri: Option<Uri>,
pub set_headers: HashMap<String, String>,
pub remove_headers: Vec<String>,
pub body: Option<Vec<u8>>,
}
impl ModifiedRequest {
pub fn new() -> Self {
Self {
method: None,
uri: None,
set_headers: HashMap::new(),
remove_headers: Vec::new(),
body: None,
}
}
#[must_use]
pub fn with_method(mut self, method: Method) -> Self {
self.method = Some(method);
self
}
#[must_use]
pub fn with_uri(mut self, uri: Uri) -> Self {
self.uri = Some(uri);
self
}
#[must_use]
pub fn with_header(mut self, name: &str, value: &str) -> Self {
self.set_headers.insert(name.to_string(), value.to_string());
self
}
#[must_use]
pub fn without_header(mut self, name: &str) -> Self {
self.remove_headers.push(name.to_string());
self
}
#[must_use]
pub fn with_body(mut self, body: Vec<u8>) -> Self {
self.body = Some(body);
self
}
pub fn apply(&self, mut request: InterceptedRequest) -> InterceptedRequest {
if let Some(method) = &self.method {
request.method = method.clone();
}
if let Some(uri) = &self.uri {
request.uri = uri.clone();
}
for name in &self.remove_headers {
if let Ok(header_name) = HeaderName::try_from(name.as_str()) {
request.headers.remove(&header_name);
}
}
for (name, value) in &self.set_headers {
if let (Ok(header_name), Ok(header_value)) = (
HeaderName::try_from(name.as_str()),
HeaderValue::try_from(value.as_str()),
) {
request.headers.insert(header_name, header_value);
}
}
if let Some(body) = &self.body {
request.body = body.clone();
}
request
}
}
impl Default for ModifiedRequest {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct ModifiedResponse {
pub status: Option<StatusCode>,
pub set_headers: HashMap<String, String>,
pub remove_headers: Vec<String>,
pub body: Option<Vec<u8>>,
}
impl ModifiedResponse {
pub fn new() -> Self {
Self {
status: None,
set_headers: HashMap::new(),
remove_headers: Vec::new(),
body: None,
}
}
#[must_use]
pub fn with_status(mut self, status: StatusCode) -> Self {
self.status = Some(status);
self
}
#[must_use]
pub fn with_header(mut self, name: &str, value: &str) -> Self {
self.set_headers.insert(name.to_string(), value.to_string());
self
}
#[must_use]
pub fn without_header(mut self, name: &str) -> Self {
self.remove_headers.push(name.to_string());
self
}
#[must_use]
pub fn with_body(mut self, body: Vec<u8>) -> Self {
self.body = Some(body);
self
}
pub fn apply(&self, mut response: InterceptedResponse) -> InterceptedResponse {
if let Some(status) = &self.status {
response.status = *status;
}
for name in &self.remove_headers {
if let Ok(header_name) = HeaderName::try_from(name.as_str()) {
response.headers.remove(&header_name);
}
}
for (name, value) in &self.set_headers {
if let (Ok(header_name), Ok(header_value)) = (
HeaderName::try_from(name.as_str()),
HeaderValue::try_from(value.as_str()),
) {
response.headers.insert(header_name, header_value);
}
}
if let Some(body) = &self.body {
response.body = body.clone();
}
response
}
}
impl Default for ModifiedResponse {
fn default() -> Self {
Self::new()
}
}
pub trait RequestInterceptor: Send + Sync {
fn intercept(&self, request: &InterceptedRequest) -> RequestAction;
}
pub trait ResponseInterceptor: Send + Sync {
fn intercept(
&self,
request: &InterceptedRequest,
response: &InterceptedResponse,
) -> ResponseAction;
}
pub struct FnRequestInterceptor<F>
where
F: Fn(&InterceptedRequest) -> RequestAction + Send + Sync,
{
f: F,
}
impl<F> FnRequestInterceptor<F>
where
F: Fn(&InterceptedRequest) -> RequestAction + Send + Sync,
{
pub fn new(f: F) -> Self {
Self { f }
}
}
impl<F> RequestInterceptor for FnRequestInterceptor<F>
where
F: Fn(&InterceptedRequest) -> RequestAction + Send + Sync,
{
fn intercept(&self, request: &InterceptedRequest) -> RequestAction {
(self.f)(request)
}
}
pub struct FnResponseInterceptor<F>
where
F: Fn(&InterceptedRequest, &InterceptedResponse) -> ResponseAction + Send + Sync,
{
f: F,
}
impl<F> FnResponseInterceptor<F>
where
F: Fn(&InterceptedRequest, &InterceptedResponse) -> ResponseAction + Send + Sync,
{
pub fn new(f: F) -> Self {
Self { f }
}
}
impl<F> ResponseInterceptor for FnResponseInterceptor<F>
where
F: Fn(&InterceptedRequest, &InterceptedResponse) -> ResponseAction + Send + Sync,
{
fn intercept(
&self,
request: &InterceptedRequest,
response: &InterceptedResponse,
) -> ResponseAction {
(self.f)(request, response)
}
}
#[derive(Debug, Clone)]
pub struct HttpExchange {
pub request: InterceptedRequest,
pub response: Option<InterceptedResponse>,
pub notes: Vec<String>,
pub tags: Vec<String>,
}
impl HttpExchange {
pub fn new(request: InterceptedRequest) -> Self {
Self {
request,
response: None,
notes: Vec::new(),
tags: Vec::new(),
}
}
pub fn with_response(mut self, response: InterceptedResponse) -> Self {
self.response = Some(response);
self
}
pub fn add_note(&mut self, note: &str) {
self.notes.push(note.to_string());
}
pub fn add_tag(&mut self, tag: &str) {
self.tags.push(tag.to_string());
}
pub fn has_tag(&self, tag: &str) -> bool {
self.tags.iter().any(|t| t == tag)
}
}
#[derive(Debug)]
pub struct HttpHistory {
exchanges: RwLock<Vec<HttpExchange>>,
max_size: usize,
}
impl HttpHistory {
pub fn new(max_size: usize) -> Self {
Self {
exchanges: RwLock::new(Vec::new()),
max_size,
}
}
pub fn add(&self, exchange: HttpExchange) {
let mut exchanges = self.exchanges.write();
if exchanges.len() >= self.max_size {
exchanges.remove(0);
}
exchanges.push(exchange);
}
pub fn get(&self, id: InterceptId) -> Option<HttpExchange> {
let exchanges = self.exchanges.read();
exchanges.iter().find(|e| e.request.id == id).cloned()
}
pub fn all(&self) -> Vec<HttpExchange> {
self.exchanges.read().clone()
}
pub fn filter<F>(&self, predicate: F) -> Vec<HttpExchange>
where
F: Fn(&HttpExchange) -> bool,
{
self.exchanges
.read()
.iter()
.filter(|e| predicate(e))
.cloned()
.collect()
}
pub fn by_host(&self, host: &str) -> Vec<HttpExchange> {
self.filter(|e| e.request.host().as_deref() == Some(host))
}
pub fn by_method(&self, method: &Method) -> Vec<HttpExchange> {
self.filter(|e| e.request.method == *method)
}
pub fn by_tag(&self, tag: &str) -> Vec<HttpExchange> {
self.filter(|e| e.has_tag(tag))
}
pub fn clear(&self) {
self.exchanges.write().clear();
}
pub fn len(&self) -> usize {
self.exchanges.read().len()
}
pub fn is_empty(&self) -> bool {
self.exchanges.read().is_empty()
}
}
impl Default for HttpHistory {
fn default() -> Self {
Self::new(10000)
}
}
pub struct InterceptProxy {
config: InterceptConfig,
id_counter: AtomicU64,
request_interceptors: RwLock<Vec<Arc<dyn RequestInterceptor>>>,
response_interceptors: RwLock<Vec<Arc<dyn ResponseInterceptor>>>,
history: Arc<HttpHistory>,
pending_requests: Mutex<HashMap<InterceptId, InterceptedRequest>>,
}
impl std::fmt::Debug for InterceptProxy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InterceptProxy")
.field("config", &self.config)
.field("id_counter", &self.id_counter)
.field(
"request_interceptors_count",
&self.request_interceptors.read().len(),
)
.field(
"response_interceptors_count",
&self.response_interceptors.read().len(),
)
.field("history", &self.history)
.finish()
}
}
impl InterceptProxy {
pub fn new(config: InterceptConfig) -> Self {
Self {
config,
id_counter: AtomicU64::new(1),
request_interceptors: RwLock::new(Vec::new()),
response_interceptors: RwLock::new(Vec::new()),
history: Arc::new(HttpHistory::default()),
pending_requests: Mutex::new(HashMap::new()),
}
}
pub fn add_request_interceptor<I: RequestInterceptor + 'static>(&self, interceptor: I) {
self.request_interceptors
.write()
.push(Arc::new(interceptor));
}
pub fn add_response_interceptor<I: ResponseInterceptor + 'static>(&self, interceptor: I) {
self.response_interceptors
.write()
.push(Arc::new(interceptor));
}
pub fn next_id(&self) -> InterceptId {
InterceptId(self.id_counter.fetch_add(1, Ordering::Relaxed))
}
pub fn intercept_request(
&self,
mut request: InterceptedRequest,
) -> (InterceptedRequest, RequestAction) {
self.pending_requests
.lock()
.insert(request.id, request.clone());
let interceptors = self.request_interceptors.read();
for interceptor in interceptors.iter() {
match interceptor.intercept(&request) {
RequestAction::Forward => continue,
RequestAction::Modify(mods) => {
request = mods.apply(request);
}
action @ (RequestAction::Drop | RequestAction::Respond(_)) => {
return (request, action);
}
}
}
(request, RequestAction::Forward)
}
pub fn intercept_response(
&self,
request_id: InterceptId,
mut response: InterceptedResponse,
) -> (InterceptedResponse, ResponseAction) {
let request = self.pending_requests.lock().remove(&request_id);
if let Some(req) = &request {
let interceptors = self.response_interceptors.read();
for interceptor in interceptors.iter() {
match interceptor.intercept(req, &response) {
ResponseAction::Forward => continue,
ResponseAction::Modify(mods) => {
response = mods.apply(response);
}
ResponseAction::Drop => {
return (response, ResponseAction::Drop);
}
}
}
let exchange = HttpExchange::new(req.clone()).with_response(response.clone());
self.history.add(exchange);
}
(response, ResponseAction::Forward)
}
pub fn history(&self) -> &Arc<HttpHistory> {
&self.history
}
pub fn config(&self) -> &InterceptConfig {
&self.config
}
}
impl Default for InterceptProxy {
fn default() -> Self {
Self::new(InterceptConfig::default())
}
}
#[derive(Debug, Clone, Default)]
pub struct InterceptStats {
pub requests_intercepted: u64,
pub responses_intercepted: u64,
pub requests_modified: u64,
pub responses_modified: u64,
pub requests_dropped: u64,
pub responses_dropped: u64,
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn test_intercept_id() {
let proxy = InterceptProxy::default();
let id1 = proxy.next_id();
let id2 = proxy.next_id();
assert_ne!(id1, id2);
}
#[test]
fn test_intercepted_request() {
let id = InterceptId(1);
let req = InterceptedRequest::new(
id,
Method::GET,
Uri::from_static("http://example.onion/test"),
)
.with_body(b"test body".to_vec());
assert_eq!(req.body_text(), Some("test body".to_string()));
}
#[test]
fn test_modified_request() {
let id = InterceptId(1);
let req =
InterceptedRequest::new(id, Method::GET, Uri::from_static("http://example.onion/"));
let mods = ModifiedRequest::new()
.with_method(Method::POST)
.with_header("X-Custom", "value");
let modified = mods.apply(req);
assert_eq!(modified.method, Method::POST);
assert_eq!(modified.header("x-custom"), Some("value"));
}
#[test]
fn test_http_history() {
let history = HttpHistory::new(100);
let req = InterceptedRequest::new(
InterceptId(1),
Method::GET,
Uri::from_static("http://example.onion/"),
);
let exchange = HttpExchange::new(req);
history.add(exchange);
assert_eq!(history.len(), 1);
let retrieved = history.get(InterceptId(1));
assert!(retrieved.is_some());
}
#[test]
fn test_fn_interceptor() {
let interceptor = FnRequestInterceptor::new(|req| {
if req.method == Method::POST {
RequestAction::Drop
} else {
RequestAction::Forward
}
});
let req = InterceptedRequest::new(
InterceptId(1),
Method::POST,
Uri::from_static("http://example.onion/"),
);
let action = interceptor.intercept(&req);
assert!(matches!(action, RequestAction::Drop));
}
}