1use super::filter;
12use super::filter::{HttpsConnectionAction, RequestAction, ResponseAction};
13use crate::llm::actions::{
14 protocol_trait::{ActionResult, Protocol, Server},
15 ActionDefinition, Parameter, ParameterDefinition,
16};
17use crate::protocol::log_template::LogTemplate;
18use crate::protocol::EventType;
19use crate::state::app_state::AppState;
20use anyhow::{Context, Result};
21use serde_json::json;
22use std::sync::LazyLock;
23
24fn parse_status(action: &serde_json::Value, default: u16) -> Result<u16> {
31 let Some(value) = action.get("status") else {
32 return Ok(default);
33 };
34 if value.is_null() {
35 return Ok(default);
36 }
37 let raw = value
38 .as_u64()
39 .with_context(|| format!("'status' must be an HTTP status code, got {value}"))?;
40 if !(100..=599).contains(&raw) {
41 return Err(anyhow::anyhow!(
42 "'status' {raw} is not an HTTP status code; use 100-599 \
43 (e.g. 403 to block a request, 502 to block a response)"
44 ));
45 }
46 Ok(raw as u16)
47}
48
49pub struct ProxyProtocol;
51
52impl ProxyProtocol {
53 pub fn new() -> Self {
54 Self
55 }
56}
57
58impl Protocol for ProxyProtocol {
60 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
61 vec![
62 ParameterDefinition {
63 name: "certificate_mode".to_string(),
64 type_hint: "string".to_string(),
65 description: "Certificate mode: 'generate' (MITM - decrypt HTTPS using a CA generated fresh at startup, requires clients to trust it) or 'none' (pass-through, no decryption, allow/block only). Default: 'none'. 'load_from_file' is not implemented and is rejected at startup.".to_string(),
66 required: false,
67 example: json!("generate"),
68 default: None,
69 },
70 ParameterDefinition {
77 name: "cert_path".to_string(),
78 type_hint: "string".to_string(),
79 description: "Path to a CA certificate in PEM format. Only read when certificate_mode is 'load_from_file', which is NOT implemented and is rejected at startup - so setting this cannot make HTTPS interception use your own CA. Use certificate_mode 'generate' with ca_export_path instead.".to_string(),
80 required: false,
81 example: json!("./ca.crt"),
82 default: None,
83 },
84 ParameterDefinition {
85 name: "key_path".to_string(),
86 type_hint: "string".to_string(),
87 description: "Path to the CA private key in PEM format. Same restriction as cert_path: only read by the unimplemented 'load_from_file' certificate mode. The key is never read by any working code path.".to_string(),
88 required: false,
89 example: json!("./ca.key"),
90 default: None,
91 },
92 ParameterDefinition {
93 name: "ca_export_path".to_string(),
94 type_hint: "string".to_string(),
95 description: "Write the generated MITM CA certificate (public certificate only, never the private key) to this path. Clients must be configured to trust this certificate or HTTPS interception will fail with an unknown-issuer error. The CA is regenerated on every server start, so a previously exported file stops working once the server restarts.".to_string(),
96 required: false,
97 example: json!("./netget-ca.crt"),
98 default: None,
99 },
100 ParameterDefinition {
101 name: "request_filter_mode".to_string(),
102 type_hint: "string".to_string(),
103 description: "Request filter mode: 'all' (intercept everything), 'match_only' (only if filters match), 'none' (pass through)".to_string(),
104 required: false,
105 example: json!("match_only"),
106 default: None,
107 },
108 ParameterDefinition {
109 name: "response_filter_mode".to_string(),
110 type_hint: "string".to_string(),
111 description: "Response filter mode: 'all', 'match_only', or 'none'".to_string(),
112 required: false,
113 example: json!("all"),
114 default: None,
115 },
116 ParameterDefinition {
117 name: "https_connection_filter_mode".to_string(),
118 type_hint: "string".to_string(),
119 description: "HTTPS connection filter mode (pass-through only): 'all', 'match_only', or 'none'".to_string(),
120 required: false,
121 example: json!("match_only"),
122 default: None,
123 },
124 ]
125 }
126 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
127 Vec::new()
142 }
143 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
144 vec![
145 handle_request_pass_action(),
147 handle_request_block_action(),
148 handle_request_modify_action(),
149 handle_response_pass_action(),
151 handle_response_block_action(),
152 handle_response_modify_action(),
153 handle_https_connection_allow_action(),
155 handle_https_connection_block_action(),
156 ]
157 }
158 fn protocol_name(&self) -> &'static str {
159 "Proxy"
160 }
161 fn get_event_types(&self) -> Vec<EventType> {
162 get_proxy_event_types()
163 }
164 fn stack_name(&self) -> &'static str {
165 "ETH>IP>TCP>HTTP>PROXY"
166 }
167 fn keywords(&self) -> Vec<&'static str> {
168 vec!["proxy", "mitm"]
169 }
170 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
171 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
172
173 ProtocolMetadataV2::builder()
174 .state(DevelopmentState::Experimental)
175 .well_known_port(8080)
176 .implementation("Manual HTTP/1.1 with rcgen v0.14 + rustls")
177 .llm_control("Request pass/block/modify, response pass/block/modify (MITM only), HTTPS allow/block")
178 .e2e_testing("reqwest configured as a real proxy client against local target servers (tests/server/proxy/test.rs), plus mocked-LLM startup and MITM scenarios (tests/server/proxy/e2e_test.rs). No curl and no browser has been run against it.")
179 .notes(
180 "OPEN RELAY BY DESIGN, AND UNRESTRICTED: the destination of every request and \
181 every CONNECT is chosen by the peer (from the request-target or the Host \
182 header), and there is no allow-list, deny-list or network restriction of any \
183 kind. Loopback, link-local (including 169.254.169.254, the cloud \
184 instance-metadata endpoint) and every RFC 1918 range are reachable, so anyone \
185 who can reach this port can reach whatever this host can -- an SSRF pivot into \
186 the operator's private network, and a relay someone else's traffic can be \
187 laundered through. The only gate is the model, and `request_filter_mode` / \
188 `https_connection_filter_mode` decide whether it is consulted at all: at \
189 `none`, or at `match_only` with filters that miss, traffic is forwarded with no \
190 consultation whatsoever. The `*_filters` lists select what the model is ASKED \
191 about; they do not restrict anything on their own. Do not expose this to an \
192 untrusted network. \
193 In MITM mode the generated CA's private key never leaves memory and is never \
194 written to disk -- `ca_export_path` writes the public certificate only -- but a \
195 client that trusts that CA has its TLS broken for every host, so the exported \
196 file must be treated as the sensitive artefact it is. \
197 HTTP/1.1 only; the MITM CA is generated per run and clients must trust it; \
198 certificate_mode 'load_from_file' is not implemented and is rejected at startup \
199 (its cert_path/key_path parameters are declared only so that rejection is the \
200 error the caller sees); responses on plain HTTP are forwarded without LLM \
201 consultation; only the first exchange of a keep-alive HTTPS connection is \
202 inspected; the request head is capped at 64 KiB with a 30s deadline and an \
203 upstream response at 8 MiB, but the number of concurrent connections is \
204 unbounded.",
205 )
206 .max_inbound_bytes(crate::server::proxy::MAX_REQUEST_BYTES)
207 .build()
208 }
209 fn description(&self) -> &'static str {
210 "HTTP/HTTPS proxy server"
211 }
212 fn example_prompt(&self) -> &'static str {
213 "Start an HTTP proxy on port 8080"
214 }
215 fn group_name(&self) -> &'static str {
216 "Proxy & Network"
217 }
218
219 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
220 use crate::llm::actions::StartupExamples;
221
222 StartupExamples::new(
223 json!({
225 "type": "open_server",
226 "port": 8080,
227 "base_stack": "proxy",
228 "instruction": "HTTP/HTTPS proxy server. Pass all HTTP requests through. For HTTPS CONNECT requests, allow all connections in pass-through mode."
229 }),
230 json!({
232 "type": "open_server",
233 "port": 8080,
234 "base_stack": "proxy",
235 "event_handlers": [{
236 "event_pattern": "proxy_http_request",
237 "handler": {
238 "type": "script",
239 "language": "python",
240 "code": "return [{\"type\": \"handle_request_pass\"}]"
241 }
242 }, {
243 "event_pattern": "proxy_https_connect",
244 "handler": {
245 "type": "script",
246 "language": "python",
247 "code": "return [{\"type\": \"handle_https_connection_allow\"}]"
248 }
249 }]
250 }),
251 json!({
253 "type": "open_server",
254 "port": 8080,
255 "base_stack": "proxy",
256 "event_handlers": [{
257 "event_pattern": "proxy_http_request",
258 "handler": {
259 "type": "static",
260 "actions": [{
261 "type": "handle_request_pass"
262 }]
263 }
264 }, {
265 "event_pattern": "proxy_https_connect",
266 "handler": {
267 "type": "static",
268 "actions": [{
269 "type": "handle_https_connection_allow"
270 }]
271 }
272 }]
273 }),
274 )
275 }
276}
277
278impl Server for ProxyProtocol {
280 fn spawn(
281 &self,
282 ctx: crate::protocol::SpawnContext,
283 ) -> std::pin::Pin<
284 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
285 > {
286 Box::pin(async move {
287 use crate::server::proxy::ProxyServer;
288 ProxyServer::spawn_with_llm_actions(
289 ctx.legacy_listen_addr(),
290 ctx.llm_client,
291 ctx.state,
292 ctx.status_tx,
293 ctx.server_id,
294 ctx.startup_params,
295 )
296 .await
297 })
298 }
299 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
300 let action_type = action
301 .get("type")
302 .and_then(|v| v.as_str())
303 .context("Missing 'type' field in action")?;
304
305 match action_type {
306 "handle_request_pass" => self.execute_handle_request_pass(action),
308 "handle_request_block" => self.execute_handle_request_block(action),
309 "handle_request_modify" => self.execute_handle_request_modify(action),
310
311 "handle_response_pass" => self.execute_handle_response_pass(action),
313 "handle_response_block" => self.execute_handle_response_block(action),
314 "handle_response_modify" => self.execute_handle_response_modify(action),
315
316 "handle_https_connection_allow" => self.execute_handle_https_connection_allow(action),
318 "handle_https_connection_block" => self.execute_handle_https_connection_block(action),
319
320 _ => Err(anyhow::anyhow!("Unknown Proxy action: {}", action_type)),
321 }
322 }
323}
324
325impl ProxyProtocol {
326 fn execute_handle_request_pass(&self, _action: serde_json::Value) -> Result<ActionResult> {
332 let result = RequestAction::Pass;
333 Ok(ActionResult::Output(
334 serde_json::to_vec(&result).context("Failed to serialize request action")?,
335 ))
336 }
337
338 fn execute_handle_request_block(&self, action: serde_json::Value) -> Result<ActionResult> {
340 let status = parse_status(&action, 403)?;
341
342 let body = action
343 .get("body")
344 .and_then(|v| v.as_str())
345 .unwrap_or("Request blocked by proxy")
346 .to_string();
347
348 let result = RequestAction::Block { status, body };
349 Ok(ActionResult::Output(
350 serde_json::to_vec(&result).context("Failed to serialize request action")?,
351 ))
352 }
353
354 fn execute_handle_request_modify(&self, action: serde_json::Value) -> Result<ActionResult> {
356 let headers: Option<std::collections::HashMap<String, String>> = action
357 .get("headers")
358 .and_then(|v| serde_json::from_value(v.clone()).ok());
359 if let Some(headers) = &headers {
360 filter::check_modify_headers(headers)?;
361 }
362
363 let remove_headers = action
364 .get("remove_headers")
365 .and_then(|v| serde_json::from_value(v.clone()).ok());
366
367 let new_path = action
368 .get("new_path")
369 .and_then(|v| v.as_str())
370 .map(|s| s.to_string());
371 if let Some(path) = &new_path {
372 filter::check_request_target_part("new_path", path)?;
373 }
374
375 let query_params: Option<std::collections::HashMap<String, String>> = action
376 .get("query_params")
377 .and_then(|v| serde_json::from_value(v.clone()).ok());
378 if let Some(params) = &query_params {
379 filter::check_query_params(params)?;
380 }
381
382 let new_body = action
383 .get("new_body")
384 .and_then(|v| v.as_str())
385 .map(|s| s.to_string());
386
387 let body_replacements = action
388 .get("body_replacements")
389 .and_then(|v| serde_json::from_value(v.clone()).ok());
390
391 let result = RequestAction::Modify {
392 headers,
393 remove_headers,
394 new_path,
395 query_params,
396 new_body,
397 body_replacements,
398 };
399
400 Ok(ActionResult::Output(
401 serde_json::to_vec(&result).context("Failed to serialize request action")?,
402 ))
403 }
404
405 fn execute_handle_response_pass(&self, _action: serde_json::Value) -> Result<ActionResult> {
411 let result = ResponseAction::Pass;
412 Ok(ActionResult::Output(
413 serde_json::to_vec(&result).context("Failed to serialize response action")?,
414 ))
415 }
416
417 fn execute_handle_response_block(&self, action: serde_json::Value) -> Result<ActionResult> {
419 let status = parse_status(&action, 502)?;
420
421 let body = action
422 .get("body")
423 .and_then(|v| v.as_str())
424 .unwrap_or("Response blocked by proxy")
425 .to_string();
426
427 let result = ResponseAction::Block { status, body };
428 Ok(ActionResult::Output(
429 serde_json::to_vec(&result).context("Failed to serialize response action")?,
430 ))
431 }
432
433 fn execute_handle_response_modify(&self, action: serde_json::Value) -> Result<ActionResult> {
435 let status = match action.get("status") {
438 None => None,
439 Some(v) if v.is_null() => None,
440 Some(_) => Some(parse_status(&action, 0)?),
441 };
442
443 let headers: Option<std::collections::HashMap<String, String>> = action
444 .get("headers")
445 .and_then(|v| serde_json::from_value(v.clone()).ok());
446 if let Some(headers) = &headers {
447 filter::check_modify_headers(headers)?;
448 }
449
450 let remove_headers = action
451 .get("remove_headers")
452 .and_then(|v| serde_json::from_value(v.clone()).ok());
453
454 let new_body = action
455 .get("new_body")
456 .and_then(|v| v.as_str())
457 .map(|s| s.to_string());
458
459 let body_replacements = action
460 .get("body_replacements")
461 .and_then(|v| serde_json::from_value(v.clone()).ok());
462
463 let result = ResponseAction::Modify {
464 status,
465 headers,
466 remove_headers,
467 new_body,
468 body_replacements,
469 };
470
471 Ok(ActionResult::Output(
472 serde_json::to_vec(&result).context("Failed to serialize response action")?,
473 ))
474 }
475
476 fn execute_handle_https_connection_allow(
482 &self,
483 _action: serde_json::Value,
484 ) -> Result<ActionResult> {
485 let result = HttpsConnectionAction::Allow;
486 Ok(ActionResult::Output(
487 serde_json::to_vec(&result).context("Failed to serialize HTTPS connection action")?,
488 ))
489 }
490
491 fn execute_handle_https_connection_block(
493 &self,
494 action: serde_json::Value,
495 ) -> Result<ActionResult> {
496 let reason = action
497 .get("reason")
498 .and_then(|v| v.as_str())
499 .map(|s| s.to_string());
500
501 let result = HttpsConnectionAction::Block { reason };
502 Ok(ActionResult::Output(
503 serde_json::to_vec(&result).context("Failed to serialize HTTPS connection action")?,
504 ))
505 }
506}
507
508fn handle_request_pass_action() -> ActionDefinition {
515 ActionDefinition {
516 name: "handle_request_pass".to_string(),
517 description: "Pass the intercepted request through unchanged to its destination"
518 .to_string(),
519 parameters: vec![],
520 example: json!({
521 "type": "handle_request_pass"
522 }),
523 log_template: Some(
524 LogTemplate::new()
525 .with_info("-> Proxy PASS request")
526 .with_debug("Proxy handle_request_pass: forwarding unchanged"),
527 ),
528 }
529}
530
531fn handle_request_block_action() -> ActionDefinition {
532 ActionDefinition {
533 name: "handle_request_block".to_string(),
534 description: "Block the intercepted request and return an error response to the client"
535 .to_string(),
536 parameters: vec![
537 Parameter {
538 name: "status".to_string(),
539 type_hint: "number".to_string(),
540 description: "HTTP status code (default: 403)".to_string(),
541 required: false,
542 },
543 Parameter {
544 name: "body".to_string(),
545 type_hint: "string".to_string(),
546 description: "Response body explaining why request was blocked".to_string(),
547 required: false,
548 },
549 ],
550 example: json!({
551 "type": "handle_request_block",
552 "status": 403,
553 "body": "Access denied by security policy"
554 }),
555 log_template: Some(
556 LogTemplate::new()
557 .with_info("-> Proxy BLOCK request {status}")
558 .with_debug("Proxy handle_request_block: status={status}"),
559 ),
560 }
561}
562
563fn handle_request_modify_action() -> ActionDefinition {
564 ActionDefinition {
565 name: "handle_request_modify".to_string(),
566 description: "Modify the intercepted request before forwarding to destination".to_string(),
567 parameters: vec![
568 Parameter {
569 name: "headers".to_string(),
570 type_hint: "object".to_string(),
571 description: "Headers to add or modify (key-value pairs)".to_string(),
572 required: false,
573 },
574 Parameter {
575 name: "remove_headers".to_string(),
576 type_hint: "array".to_string(),
577 description: "Header names to remove".to_string(),
578 required: false,
579 },
580 Parameter {
581 name: "new_path".to_string(),
582 type_hint: "string".to_string(),
583 description: "New URL path (replaces entire path)".to_string(),
584 required: false,
585 },
586 Parameter {
587 name: "query_params".to_string(),
588 type_hint: "object".to_string(),
589 description: "Query parameters to add/modify".to_string(),
590 required: false,
591 },
592 Parameter {
593 name: "new_body".to_string(),
594 type_hint: "string".to_string(),
595 description: "Complete body replacement".to_string(),
596 required: false,
597 },
598 Parameter {
599 name: "body_replacements".to_string(),
600 type_hint: "array".to_string(),
601 description:
602 "Array of regex replacements: [{pattern: 'regex', replacement: 'text'}]"
603 .to_string(),
604 required: false,
605 },
606 ],
607 example: json!({
608 "type": "handle_request_modify",
609 "headers": {
610 "X-Proxy-Modified": "true",
611 "User-Agent": "CustomBot/1.0"
612 },
613 "remove_headers": ["Cookie"],
614 "body_replacements": [
615 {
616 "pattern": "password",
617 "replacement": "****REDACTED****"
618 }
619 ]
620 }),
621 log_template: Some(
622 LogTemplate::new()
623 .with_info("-> Proxy MODIFY request")
624 .with_debug("Proxy handle_request_modify: path={new_path}"),
625 ),
626 }
627}
628
629fn handle_response_pass_action() -> ActionDefinition {
632 ActionDefinition {
633 name: "handle_response_pass".to_string(),
634 description: "Pass the intercepted response through unchanged to the client".to_string(),
635 parameters: vec![],
636 example: json!({
637 "type": "handle_response_pass"
638 }),
639 log_template: Some(
640 LogTemplate::new()
641 .with_info("-> Proxy PASS response")
642 .with_debug("Proxy handle_response_pass: forwarding unchanged"),
643 ),
644 }
645}
646
647fn handle_response_block_action() -> ActionDefinition {
648 ActionDefinition {
649 name: "handle_response_block".to_string(),
650 description: "Block the intercepted response and return a different response to the client"
651 .to_string(),
652 parameters: vec![
653 Parameter {
654 name: "status".to_string(),
655 type_hint: "number".to_string(),
656 description: "HTTP status code (default: 502)".to_string(),
657 required: false,
658 },
659 Parameter {
660 name: "body".to_string(),
661 type_hint: "string".to_string(),
662 description: "Response body".to_string(),
663 required: false,
664 },
665 ],
666 example: json!({
667 "type": "handle_response_block",
668 "status": 502,
669 "body": "Response blocked by content policy"
670 }),
671 log_template: Some(
672 LogTemplate::new()
673 .with_info("-> Proxy BLOCK response {status}")
674 .with_debug("Proxy handle_response_block: status={status}"),
675 ),
676 }
677}
678
679fn handle_response_modify_action() -> ActionDefinition {
680 ActionDefinition {
681 name: "handle_response_modify".to_string(),
682 description: "Modify the intercepted response before returning to client".to_string(),
683 parameters: vec![
684 Parameter {
685 name: "status".to_string(),
686 type_hint: "number".to_string(),
687 description: "New HTTP status code".to_string(),
688 required: false,
689 },
690 Parameter {
691 name: "headers".to_string(),
692 type_hint: "object".to_string(),
693 description: "Headers to add or modify (key-value pairs)".to_string(),
694 required: false,
695 },
696 Parameter {
697 name: "remove_headers".to_string(),
698 type_hint: "array".to_string(),
699 description: "Header names to remove".to_string(),
700 required: false,
701 },
702 Parameter {
703 name: "new_body".to_string(),
704 type_hint: "string".to_string(),
705 description: "Complete body replacement".to_string(),
706 required: false,
707 },
708 Parameter {
709 name: "body_replacements".to_string(),
710 type_hint: "array".to_string(),
711 description:
712 "Array of regex replacements: [{pattern: 'regex', replacement: 'text'}]"
713 .to_string(),
714 required: false,
715 },
716 ],
717 example: json!({
718 "type": "handle_response_modify",
719 "headers": {
720 "X-Content-Filtered": "true"
721 },
722 "body_replacements": [
723 {
724 "pattern": "secret-api-key-\\w+",
725 "replacement": "****REDACTED****"
726 }
727 ]
728 }),
729 log_template: Some(
730 LogTemplate::new()
731 .with_info("-> Proxy MODIFY response")
732 .with_debug("Proxy handle_response_modify: status={status}"),
733 ),
734 }
735}
736
737fn handle_https_connection_allow_action() -> ActionDefinition {
740 ActionDefinition {
741 name: "handle_https_connection_allow".to_string(),
742 description: "Allow HTTPS connection to proceed (pass-through mode only, no MITM)"
743 .to_string(),
744 parameters: vec![],
745 example: json!({
746 "type": "handle_https_connection_allow"
747 }),
748 log_template: Some(
749 LogTemplate::new()
750 .with_info("-> Proxy ALLOW HTTPS")
751 .with_debug("Proxy handle_https_connection_allow: tunnel established"),
752 ),
753 }
754}
755
756fn handle_https_connection_block_action() -> ActionDefinition {
757 ActionDefinition {
758 name: "handle_https_connection_block".to_string(),
759 description: "Block HTTPS connection (pass-through mode only, no MITM)".to_string(),
760 parameters: vec![Parameter {
761 name: "reason".to_string(),
762 type_hint: "string".to_string(),
763 description: "Optional reason for blocking".to_string(),
764 required: false,
765 }],
766 example: json!({
767 "type": "handle_https_connection_block",
768 "reason": "Destination blocked by security policy"
769 }),
770 log_template: Some(
771 LogTemplate::new()
772 .with_info("-> Proxy BLOCK HTTPS")
773 .with_debug("Proxy handle_https_connection_block: reason={reason}"),
774 ),
775 }
776}
777
778pub static PROXY_HTTP_REQUEST_EVENT: LazyLock<EventType> = LazyLock::new(|| {
784 EventType::new(
785 "proxy_http_request",
786 "HTTP request intercepted by proxy",
787 json!({"type": "placeholder", "event_id": "proxy_http_request"}),
788 )
789 .with_parameters(vec![
790 Parameter {
791 name: "method".to_string(),
792 type_hint: "string".to_string(),
793 description: "HTTP method (GET, POST, etc.)".to_string(),
794 required: true,
795 },
796 Parameter {
797 name: "url".to_string(),
798 type_hint: "string".to_string(),
799 description: "Full request URL".to_string(),
800 required: true,
801 },
802 Parameter {
803 name: "host".to_string(),
804 type_hint: "string".to_string(),
805 description: "Host header value".to_string(),
806 required: true,
807 },
808 Parameter {
809 name: "path".to_string(),
810 type_hint: "string".to_string(),
811 description: "Request path".to_string(),
812 required: true,
813 },
814 ])
815 .with_actions(vec![
816 handle_request_pass_action(),
817 handle_request_block_action(),
818 handle_request_modify_action(),
819 ])
820 .with_log_template(
821 LogTemplate::new()
822 .with_info("Proxy {client_ip} {method} {host}")
823 .with_debug("HTTP proxy {method} to {host} from {client_ip}:{client_port}")
824 .with_trace("Proxy: {json_pretty(.)}"),
825 )
826});
827
828pub static PROXY_HTTP_RESPONSE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
830 EventType::new(
831 "proxy_http_response",
832 "HTTP response received from upstream server",
833 json!({"type": "placeholder", "event_id": "proxy_http_response"}),
834 )
835 .with_parameters(vec![
836 Parameter {
837 name: "status_code".to_string(),
838 type_hint: "number".to_string(),
839 description: "HTTP status code (200, 404, etc.)".to_string(),
840 required: true,
841 },
842 Parameter {
843 name: "url".to_string(),
844 type_hint: "string".to_string(),
845 description: "Original request URL".to_string(),
846 required: true,
847 },
848 Parameter {
849 name: "headers".to_string(),
850 type_hint: "object".to_string(),
851 description: "Response headers as key-value pairs".to_string(),
852 required: true,
853 },
854 Parameter {
855 name: "body".to_string(),
856 type_hint: "string".to_string(),
857 description: "Response body (may be truncated for large responses)".to_string(),
858 required: false,
859 },
860 ])
861 .with_actions(vec![
862 handle_response_pass_action(),
863 handle_response_block_action(),
864 handle_response_modify_action(),
865 ])
866 .with_log_template(
867 LogTemplate::new()
868 .with_info("Proxy {client_ip} response {status_code}")
869 .with_debug("HTTP proxy response {status_code} from {url}")
870 .with_trace("Proxy: {json_pretty(.)}"),
871 )
872});
873
874pub static PROXY_HTTPS_CONNECT_EVENT: LazyLock<EventType> = LazyLock::new(|| {
876 EventType::new(
877 "proxy_https_connect",
878 "HTTPS CONNECT request intercepted by proxy (pass-through mode)",
879 json!({"type": "handle_https_connection_allow"}),
880 )
881 .with_parameters(vec![
882 Parameter {
883 name: "destination_host".to_string(),
884 type_hint: "string".to_string(),
885 description: "Destination hostname".to_string(),
886 required: true,
887 },
888 Parameter {
889 name: "destination_port".to_string(),
890 type_hint: "number".to_string(),
891 description: "Destination port".to_string(),
892 required: true,
893 },
894 Parameter {
895 name: "sni".to_string(),
896 type_hint: "string".to_string(),
897 description: "SNI (Server Name Indication) from TLS handshake".to_string(),
898 required: false,
899 },
900 ])
901 .with_actions(vec![
902 handle_https_connection_allow_action(),
903 handle_https_connection_block_action(),
904 ])
905 .with_log_template(
906 LogTemplate::new()
907 .with_info("Proxy {client_ip} CONNECT {destination_host}:{destination_port}")
908 .with_debug("HTTPS CONNECT to {destination_host}:{destination_port} from {client_ip}:{client_port}")
909 .with_trace("Proxy: {json_pretty(.)}"),
910 )
911});
912
913pub fn get_proxy_event_types() -> Vec<EventType> {
915 vec![
916 PROXY_HTTP_REQUEST_EVENT.clone(),
917 PROXY_HTTP_RESPONSE_EVENT.clone(),
918 PROXY_HTTPS_CONNECT_EVENT.clone(),
919 ]
920}