1use regex::Regex;
31use serde::{Deserialize, Serialize};
32use std::collections::HashMap;
33use std::path::PathBuf;
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37#[serde(tag = "mode", rename_all = "lowercase")]
38pub enum CertificateMode {
39 Generate,
41
42 LoadFromFile {
44 cert_path: PathBuf,
46 key_path: PathBuf,
48 },
49
50 None,
53}
54
55impl Default for CertificateMode {
56 fn default() -> Self {
57 CertificateMode::Generate
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct HttpsConnectionInfo {
64 pub destination_host: String,
66 pub destination_port: u16,
68 pub sni: Option<String>,
70 pub client_addr: String,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct FullRequestInfo {
77 pub method: String,
79 pub url: String,
81 pub path: String,
83 pub host: String,
85 pub headers: HashMap<String, String>,
87 pub body: Vec<u8>,
89 pub client_addr: String,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct FullResponseInfo {
96 pub status: u16,
98 pub headers: HashMap<String, String>,
100 pub body: Vec<u8>,
102 pub request_host: String,
104 pub request_path: String,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct RequestFilter {
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub host_regex: Option<String>,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub path_regex: Option<String>,
118
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub method_regex: Option<String>,
122
123 #[serde(skip_serializing_if = "Option::is_none")]
125 pub header_regex: Option<String>,
126
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub body_regex: Option<String>,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct HttpsConnectionFilter {
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub host_regex: Option<String>,
138
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub port_regex: Option<String>,
142
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub sni_regex: Option<String>,
146
147 #[serde(skip_serializing_if = "Option::is_none")]
149 pub client_addr_regex: Option<String>,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct ResponseFilter {
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub status_regex: Option<String>,
158
159 #[serde(skip_serializing_if = "Option::is_none")]
161 pub header_regex: Option<String>,
162
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub body_regex: Option<String>,
166
167 #[serde(skip_serializing_if = "Option::is_none")]
169 pub request_host_regex: Option<String>,
170
171 #[serde(skip_serializing_if = "Option::is_none")]
173 pub request_path_regex: Option<String>,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178#[serde(tag = "action", rename_all = "lowercase")]
179pub enum RequestAction {
180 Pass,
182
183 Block {
185 #[serde(default = "default_block_status")]
187 status: u16,
188 #[serde(default = "default_block_body")]
190 body: String,
191 },
192
193 Modify {
195 #[serde(skip_serializing_if = "Option::is_none")]
197 headers: Option<HashMap<String, String>>,
198
199 #[serde(skip_serializing_if = "Option::is_none")]
201 remove_headers: Option<Vec<String>>,
202
203 #[serde(skip_serializing_if = "Option::is_none")]
205 new_path: Option<String>,
206
207 #[serde(skip_serializing_if = "Option::is_none")]
209 query_params: Option<HashMap<String, String>>,
210
211 #[serde(skip_serializing_if = "Option::is_none")]
213 new_body: Option<String>,
214
215 #[serde(skip_serializing_if = "Option::is_none")]
217 body_replacements: Option<Vec<RegexReplacement>>,
218 },
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
223#[serde(tag = "action", rename_all = "lowercase")]
224pub enum ResponseAction {
225 Pass,
227
228 Block {
230 #[serde(default = "default_response_block_status")]
232 status: u16,
233 #[serde(default = "default_response_block_body")]
235 body: String,
236 },
237
238 Modify {
240 #[serde(skip_serializing_if = "Option::is_none")]
242 status: Option<u16>,
243
244 #[serde(skip_serializing_if = "Option::is_none")]
246 headers: Option<HashMap<String, String>>,
247
248 #[serde(skip_serializing_if = "Option::is_none")]
250 remove_headers: Option<Vec<String>>,
251
252 #[serde(skip_serializing_if = "Option::is_none")]
254 new_body: Option<String>,
255
256 #[serde(skip_serializing_if = "Option::is_none")]
258 body_replacements: Option<Vec<RegexReplacement>>,
259 },
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct RegexReplacement {
265 pub pattern: String,
267 pub replacement: String,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
273#[serde(tag = "action", rename_all = "lowercase")]
274pub enum HttpsConnectionAction {
275 Allow,
277
278 Block {
280 #[serde(skip_serializing_if = "Option::is_none")]
282 reason: Option<String>,
283 },
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct ProxyFilterConfig {
289 #[serde(default)]
291 pub certificate_mode: CertificateMode,
292
293 #[serde(default)]
295 pub request_filters: Vec<RequestFilter>,
296
297 #[serde(default)]
299 pub response_filters: Vec<ResponseFilter>,
300
301 #[serde(default)]
303 pub https_connection_filters: Vec<HttpsConnectionFilter>,
304
305 #[serde(default)]
307 pub request_filter_mode: FilterMode,
308
309 #[serde(default)]
311 pub response_filter_mode: FilterMode,
312
313 #[serde(default)]
315 pub https_connection_filter_mode: FilterMode,
316}
317
318impl Default for ProxyFilterConfig {
319 fn default() -> Self {
320 Self {
321 certificate_mode: CertificateMode::None, request_filters: Vec::new(),
323 response_filters: Vec::new(),
324 https_connection_filters: Vec::new(),
325 request_filter_mode: FilterMode::All,
326 response_filter_mode: FilterMode::All,
327 https_connection_filter_mode: FilterMode::All,
328 }
329 }
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
334#[serde(rename_all = "lowercase")]
335pub enum FilterMode {
336 #[default]
338 All,
339
340 MatchOnly,
342
343 None,
345}
346
347pub const FRAMING_HEADERS: [&str; 2] = ["content-length", "transfer-encoding"];
354
355pub fn check_header_name(name: &str) -> anyhow::Result<()> {
364 anyhow::ensure!(
365 !name.is_empty()
366 && name.len() <= 256
367 && name
368 .bytes()
369 .all(|b| b.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&b)),
370 "header name {name:?} is not an HTTP token"
371 );
372 Ok(())
373}
374
375pub fn check_header_value(name: &str, value: &str) -> anyhow::Result<()> {
377 anyhow::ensure!(
378 value.len() <= 8192 && !value.chars().any(|c| c.is_control() && c != '\t'),
379 "header {name:?} contains a control character (CR/LF would split the message)"
380 );
381 Ok(())
382}
383
384pub fn check_modify_headers(headers: &HashMap<String, String>) -> anyhow::Result<()> {
386 anyhow::ensure!(headers.len() <= 64, "at most 64 headers");
387 for (name, value) in headers {
388 check_header_name(name)?;
389 check_header_value(name, value)?;
390 anyhow::ensure!(
391 !FRAMING_HEADERS.contains(&name.to_ascii_lowercase().as_str()),
392 "header {name:?} is set by the proxy from the body it writes; it cannot be added \
393 (it may be named in remove_headers)"
394 );
395 }
396 Ok(())
397}
398
399pub fn check_request_target_part(what: &str, value: &str) -> anyhow::Result<()> {
403 anyhow::ensure!(
404 value.len() <= 8192 && !value.chars().any(|c| c.is_ascii_whitespace() || c.is_control()),
405 "{what} {value:?} contains whitespace or a control character; it would end the request line"
406 );
407 Ok(())
408}
409
410pub fn check_query_params(params: &HashMap<String, String>) -> anyhow::Result<()> {
412 anyhow::ensure!(params.len() <= 64, "at most 64 query_params");
413 for (name, value) in params {
414 check_request_target_part("query_params name", name)?;
415 check_request_target_part("query_params value", value)?;
416 }
417 Ok(())
418}
419
420fn default_block_status() -> u16 {
421 403
422}
423fn default_block_body() -> String {
424 "Request blocked by proxy".to_string()
425}
426fn default_response_block_status() -> u16 {
427 502
428}
429fn default_response_block_body() -> String {
430 "Response blocked by proxy".to_string()
431}
432
433impl RequestFilter {
434 pub fn matches(
436 &self,
437 host: &str,
438 path: &str,
439 method: &str,
440 headers: &HashMap<String, String>,
441 body: &[u8],
442 ) -> bool {
443 if let Some(pattern) = &self.host_regex {
446 if let Ok(re) = Regex::new(pattern) {
447 if !re.is_match(host) {
448 return false;
449 }
450 }
451 }
452
453 if let Some(pattern) = &self.path_regex {
454 if let Ok(re) = Regex::new(pattern) {
455 if !re.is_match(path) {
456 return false;
457 }
458 }
459 }
460
461 if let Some(pattern) = &self.method_regex {
462 if let Ok(re) = Regex::new(pattern) {
463 if !re.is_match(method) {
464 return false;
465 }
466 }
467 }
468
469 if let Some(pattern) = &self.header_regex {
470 if let Ok(re) = Regex::new(pattern) {
471 let headers_text: String = headers
473 .iter()
474 .map(|(k, v)| format!("{}: {}", k, v))
475 .collect::<Vec<_>>()
476 .join("\n");
477
478 if !re.is_match(&headers_text) {
479 return false;
480 }
481 }
482 }
483
484 if let Some(pattern) = &self.body_regex {
485 if let Ok(re) = Regex::new(pattern) {
486 if let Ok(body_str) = std::str::from_utf8(body) {
487 if !re.is_match(body_str) {
488 return false;
489 }
490 } else {
491 return false;
493 }
494 }
495 }
496
497 true
499 }
500}
501
502impl HttpsConnectionFilter {
503 pub fn matches(
505 &self,
506 destination_host: &str,
507 destination_port: u16,
508 sni: Option<&str>,
509 client_addr: &str,
510 ) -> bool {
511 if let Some(pattern) = &self.host_regex {
514 if let Ok(re) = Regex::new(pattern) {
515 if !re.is_match(destination_host) {
516 return false;
517 }
518 }
519 }
520
521 if let Some(pattern) = &self.port_regex {
522 if let Ok(re) = Regex::new(pattern) {
523 if !re.is_match(&destination_port.to_string()) {
524 return false;
525 }
526 }
527 }
528
529 if let Some(pattern) = &self.sni_regex {
530 if let Ok(re) = Regex::new(pattern) {
531 if let Some(sni_value) = sni {
532 if !re.is_match(sni_value) {
533 return false;
534 }
535 } else {
536 return false;
538 }
539 }
540 }
541
542 if let Some(pattern) = &self.client_addr_regex {
543 if let Ok(re) = Regex::new(pattern) {
544 if !re.is_match(client_addr) {
545 return false;
546 }
547 }
548 }
549
550 true
552 }
553}
554
555impl ResponseFilter {
556 pub fn matches(
558 &self,
559 status: u16,
560 headers: &HashMap<String, String>,
561 body: &[u8],
562 request_host: Option<&str>,
563 request_path: Option<&str>,
564 ) -> bool {
565 if let Some(pattern) = &self.status_regex {
568 if let Ok(re) = Regex::new(pattern) {
569 if !re.is_match(&status.to_string()) {
570 return false;
571 }
572 }
573 }
574
575 if let Some(pattern) = &self.header_regex {
576 if let Ok(re) = Regex::new(pattern) {
577 let headers_text: String = headers
579 .iter()
580 .map(|(k, v)| format!("{}: {}", k, v))
581 .collect::<Vec<_>>()
582 .join("\n");
583
584 if !re.is_match(&headers_text) {
585 return false;
586 }
587 }
588 }
589
590 if let Some(pattern) = &self.body_regex {
591 if let Ok(re) = Regex::new(pattern) {
592 if let Ok(body_str) = std::str::from_utf8(body) {
593 if !re.is_match(body_str) {
594 return false;
595 }
596 } else {
597 return false;
598 }
599 }
600 }
601
602 if let Some(pattern) = &self.request_host_regex {
603 if let Ok(re) = Regex::new(pattern) {
604 if let Some(host) = request_host {
605 if !re.is_match(host) {
606 return false;
607 }
608 } else {
609 return false;
610 }
611 }
612 }
613
614 if let Some(pattern) = &self.request_path_regex {
615 if let Ok(re) = Regex::new(pattern) {
616 if let Some(path) = request_path {
617 if !re.is_match(path) {
618 return false;
619 }
620 } else {
621 return false;
622 }
623 }
624 }
625
626 true
628 }
629}
630
631impl ProxyFilterConfig {
632 pub fn is_mitm_mode(&self) -> bool {
634 !matches!(self.certificate_mode, CertificateMode::None)
635 }
636
637 pub fn should_intercept_request(
639 &self,
640 host: &str,
641 path: &str,
642 method: &str,
643 headers: &HashMap<String, String>,
644 body: &[u8],
645 ) -> bool {
646 match self.request_filter_mode {
647 FilterMode::All => true,
648 FilterMode::None => false,
649 FilterMode::MatchOnly => {
650 if self.request_filters.is_empty() {
651 false
653 } else {
654 self.request_filters
656 .iter()
657 .any(|f| f.matches(host, path, method, headers, body))
658 }
659 }
660 }
661 }
662
663 pub fn should_intercept_response(
665 &self,
666 status: u16,
667 headers: &HashMap<String, String>,
668 body: &[u8],
669 request_host: Option<&str>,
670 request_path: Option<&str>,
671 ) -> bool {
672 match self.response_filter_mode {
673 FilterMode::All => true,
674 FilterMode::None => false,
675 FilterMode::MatchOnly => {
676 if self.response_filters.is_empty() {
677 false
679 } else {
680 self.response_filters
682 .iter()
683 .any(|f| f.matches(status, headers, body, request_host, request_path))
684 }
685 }
686 }
687 }
688
689 pub fn should_intercept_https_connection(
691 &self,
692 destination_host: &str,
693 destination_port: u16,
694 sni: Option<&str>,
695 client_addr: &str,
696 ) -> bool {
697 if self.is_mitm_mode() {
699 return false;
700 }
701
702 match self.https_connection_filter_mode {
703 FilterMode::All => true,
704 FilterMode::None => false,
705 FilterMode::MatchOnly => {
706 if self.https_connection_filters.is_empty() {
707 false
709 } else {
710 self.https_connection_filters
712 .iter()
713 .any(|f| f.matches(destination_host, destination_port, sni, client_addr))
714 }
715 }
716 }
717 }
718
719 pub fn should_inspect_response(&self, _request_info: &FullRequestInfo) -> bool {
721 match self.response_filter_mode {
724 FilterMode::None => false,
725 FilterMode::All => true,
726 FilterMode::MatchOnly => {
727 !self.response_filters.is_empty()
730 }
731 }
732 }
733}