use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "mode", rename_all = "lowercase")]
pub enum CertificateMode {
Generate,
LoadFromFile {
cert_path: PathBuf,
key_path: PathBuf,
},
None,
}
impl Default for CertificateMode {
fn default() -> Self {
CertificateMode::Generate
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpsConnectionInfo {
pub destination_host: String,
pub destination_port: u16,
pub sni: Option<String>,
pub client_addr: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FullRequestInfo {
pub method: String,
pub url: String,
pub path: String,
pub host: String,
pub headers: HashMap<String, String>,
pub body: Vec<u8>,
pub client_addr: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FullResponseInfo {
pub status: u16,
pub headers: HashMap<String, String>,
pub body: Vec<u8>,
pub request_host: String,
pub request_path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RequestFilter {
#[serde(skip_serializing_if = "Option::is_none")]
pub host_regex: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub path_regex: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub method_regex: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub header_regex: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body_regex: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpsConnectionFilter {
#[serde(skip_serializing_if = "Option::is_none")]
pub host_regex: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub port_regex: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sni_regex: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_addr_regex: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseFilter {
#[serde(skip_serializing_if = "Option::is_none")]
pub status_regex: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub header_regex: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body_regex: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_host_regex: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_path_regex: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "action", rename_all = "lowercase")]
pub enum RequestAction {
Pass,
Block {
#[serde(default = "default_block_status")]
status: u16,
#[serde(default = "default_block_body")]
body: String,
},
Modify {
#[serde(skip_serializing_if = "Option::is_none")]
headers: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
remove_headers: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
new_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
query_params: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
new_body: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
body_replacements: Option<Vec<RegexReplacement>>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "action", rename_all = "lowercase")]
pub enum ResponseAction {
Pass,
Block {
#[serde(default = "default_response_block_status")]
status: u16,
#[serde(default = "default_response_block_body")]
body: String,
},
Modify {
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
headers: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
remove_headers: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
new_body: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
body_replacements: Option<Vec<RegexReplacement>>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegexReplacement {
pub pattern: String,
pub replacement: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "action", rename_all = "lowercase")]
pub enum HttpsConnectionAction {
Allow,
Block {
#[serde(skip_serializing_if = "Option::is_none")]
reason: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProxyFilterConfig {
#[serde(default)]
pub certificate_mode: CertificateMode,
#[serde(default)]
pub request_filters: Vec<RequestFilter>,
#[serde(default)]
pub response_filters: Vec<ResponseFilter>,
#[serde(default)]
pub https_connection_filters: Vec<HttpsConnectionFilter>,
#[serde(default)]
pub request_filter_mode: FilterMode,
#[serde(default)]
pub response_filter_mode: FilterMode,
#[serde(default)]
pub https_connection_filter_mode: FilterMode,
}
impl Default for ProxyFilterConfig {
fn default() -> Self {
Self {
certificate_mode: CertificateMode::None, request_filters: Vec::new(),
response_filters: Vec::new(),
https_connection_filters: Vec::new(),
request_filter_mode: FilterMode::All,
response_filter_mode: FilterMode::All,
https_connection_filter_mode: FilterMode::All,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum FilterMode {
#[default]
All,
MatchOnly,
None,
}
pub const FRAMING_HEADERS: [&str; 2] = ["content-length", "transfer-encoding"];
pub fn check_header_name(name: &str) -> anyhow::Result<()> {
anyhow::ensure!(
!name.is_empty()
&& name.len() <= 256
&& name
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&b)),
"header name {name:?} is not an HTTP token"
);
Ok(())
}
pub fn check_header_value(name: &str, value: &str) -> anyhow::Result<()> {
anyhow::ensure!(
value.len() <= 8192 && !value.chars().any(|c| c.is_control() && c != '\t'),
"header {name:?} contains a control character (CR/LF would split the message)"
);
Ok(())
}
pub fn check_modify_headers(headers: &HashMap<String, String>) -> anyhow::Result<()> {
anyhow::ensure!(headers.len() <= 64, "at most 64 headers");
for (name, value) in headers {
check_header_name(name)?;
check_header_value(name, value)?;
anyhow::ensure!(
!FRAMING_HEADERS.contains(&name.to_ascii_lowercase().as_str()),
"header {name:?} is set by the proxy from the body it writes; it cannot be added \
(it may be named in remove_headers)"
);
}
Ok(())
}
pub fn check_request_target_part(what: &str, value: &str) -> anyhow::Result<()> {
anyhow::ensure!(
value.len() <= 8192 && !value.chars().any(|c| c.is_ascii_whitespace() || c.is_control()),
"{what} {value:?} contains whitespace or a control character; it would end the request line"
);
Ok(())
}
pub fn check_query_params(params: &HashMap<String, String>) -> anyhow::Result<()> {
anyhow::ensure!(params.len() <= 64, "at most 64 query_params");
for (name, value) in params {
check_request_target_part("query_params name", name)?;
check_request_target_part("query_params value", value)?;
}
Ok(())
}
fn default_block_status() -> u16 {
403
}
fn default_block_body() -> String {
"Request blocked by proxy".to_string()
}
fn default_response_block_status() -> u16 {
502
}
fn default_response_block_body() -> String {
"Response blocked by proxy".to_string()
}
impl RequestFilter {
pub fn matches(
&self,
host: &str,
path: &str,
method: &str,
headers: &HashMap<String, String>,
body: &[u8],
) -> bool {
if let Some(pattern) = &self.host_regex {
if let Ok(re) = Regex::new(pattern) {
if !re.is_match(host) {
return false;
}
}
}
if let Some(pattern) = &self.path_regex {
if let Ok(re) = Regex::new(pattern) {
if !re.is_match(path) {
return false;
}
}
}
if let Some(pattern) = &self.method_regex {
if let Ok(re) = Regex::new(pattern) {
if !re.is_match(method) {
return false;
}
}
}
if let Some(pattern) = &self.header_regex {
if let Ok(re) = Regex::new(pattern) {
let headers_text: String = headers
.iter()
.map(|(k, v)| format!("{}: {}", k, v))
.collect::<Vec<_>>()
.join("\n");
if !re.is_match(&headers_text) {
return false;
}
}
}
if let Some(pattern) = &self.body_regex {
if let Ok(re) = Regex::new(pattern) {
if let Ok(body_str) = std::str::from_utf8(body) {
if !re.is_match(body_str) {
return false;
}
} else {
return false;
}
}
}
true
}
}
impl HttpsConnectionFilter {
pub fn matches(
&self,
destination_host: &str,
destination_port: u16,
sni: Option<&str>,
client_addr: &str,
) -> bool {
if let Some(pattern) = &self.host_regex {
if let Ok(re) = Regex::new(pattern) {
if !re.is_match(destination_host) {
return false;
}
}
}
if let Some(pattern) = &self.port_regex {
if let Ok(re) = Regex::new(pattern) {
if !re.is_match(&destination_port.to_string()) {
return false;
}
}
}
if let Some(pattern) = &self.sni_regex {
if let Ok(re) = Regex::new(pattern) {
if let Some(sni_value) = sni {
if !re.is_match(sni_value) {
return false;
}
} else {
return false;
}
}
}
if let Some(pattern) = &self.client_addr_regex {
if let Ok(re) = Regex::new(pattern) {
if !re.is_match(client_addr) {
return false;
}
}
}
true
}
}
impl ResponseFilter {
pub fn matches(
&self,
status: u16,
headers: &HashMap<String, String>,
body: &[u8],
request_host: Option<&str>,
request_path: Option<&str>,
) -> bool {
if let Some(pattern) = &self.status_regex {
if let Ok(re) = Regex::new(pattern) {
if !re.is_match(&status.to_string()) {
return false;
}
}
}
if let Some(pattern) = &self.header_regex {
if let Ok(re) = Regex::new(pattern) {
let headers_text: String = headers
.iter()
.map(|(k, v)| format!("{}: {}", k, v))
.collect::<Vec<_>>()
.join("\n");
if !re.is_match(&headers_text) {
return false;
}
}
}
if let Some(pattern) = &self.body_regex {
if let Ok(re) = Regex::new(pattern) {
if let Ok(body_str) = std::str::from_utf8(body) {
if !re.is_match(body_str) {
return false;
}
} else {
return false;
}
}
}
if let Some(pattern) = &self.request_host_regex {
if let Ok(re) = Regex::new(pattern) {
if let Some(host) = request_host {
if !re.is_match(host) {
return false;
}
} else {
return false;
}
}
}
if let Some(pattern) = &self.request_path_regex {
if let Ok(re) = Regex::new(pattern) {
if let Some(path) = request_path {
if !re.is_match(path) {
return false;
}
} else {
return false;
}
}
}
true
}
}
impl ProxyFilterConfig {
pub fn is_mitm_mode(&self) -> bool {
!matches!(self.certificate_mode, CertificateMode::None)
}
pub fn should_intercept_request(
&self,
host: &str,
path: &str,
method: &str,
headers: &HashMap<String, String>,
body: &[u8],
) -> bool {
match self.request_filter_mode {
FilterMode::All => true,
FilterMode::None => false,
FilterMode::MatchOnly => {
if self.request_filters.is_empty() {
false
} else {
self.request_filters
.iter()
.any(|f| f.matches(host, path, method, headers, body))
}
}
}
}
pub fn should_intercept_response(
&self,
status: u16,
headers: &HashMap<String, String>,
body: &[u8],
request_host: Option<&str>,
request_path: Option<&str>,
) -> bool {
match self.response_filter_mode {
FilterMode::All => true,
FilterMode::None => false,
FilterMode::MatchOnly => {
if self.response_filters.is_empty() {
false
} else {
self.response_filters
.iter()
.any(|f| f.matches(status, headers, body, request_host, request_path))
}
}
}
}
pub fn should_intercept_https_connection(
&self,
destination_host: &str,
destination_port: u16,
sni: Option<&str>,
client_addr: &str,
) -> bool {
if self.is_mitm_mode() {
return false;
}
match self.https_connection_filter_mode {
FilterMode::All => true,
FilterMode::None => false,
FilterMode::MatchOnly => {
if self.https_connection_filters.is_empty() {
false
} else {
self.https_connection_filters
.iter()
.any(|f| f.matches(destination_host, destination_port, sni, client_addr))
}
}
}
}
pub fn should_inspect_response(&self, _request_info: &FullRequestInfo) -> bool {
match self.response_filter_mode {
FilterMode::None => false,
FilterMode::All => true,
FilterMode::MatchOnly => {
!self.response_filters.is_empty()
}
}
}
}