1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, Parameter, ParameterDefinition,
7};
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14pub static HTTP_PROXY_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16 EventType::new(
17 "http_proxy_connected",
18 "HTTP proxy client connected to proxy server",
19 json!({
20 "type": "establish_tunnel",
21 "target_host": "example.com",
22 "target_port": 443
23 }),
24 )
25 .with_parameters(vec![Parameter {
26 name: "proxy_addr".to_string(),
27 type_hint: "string".to_string(),
28 description: "HTTP proxy server address".to_string(),
29 required: true,
30 }])
31});
32
33pub static HTTP_PROXY_TUNNEL_ESTABLISHED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
35 EventType::new(
36 "http_proxy_tunnel_established",
37 "HTTP CONNECT tunnel successfully established",
38 json!({
39 "type": "send_http_request",
40 "method": "GET",
41 "path": "/",
42 "headers": {"Host": "example.com"}
43 }),
44 )
45 .with_parameters(vec![
46 Parameter {
47 name: "target_host".to_string(),
48 type_hint: "string".to_string(),
49 description: "Target host for tunnel".to_string(),
50 required: true,
51 },
52 Parameter {
53 name: "target_port".to_string(),
54 type_hint: "number".to_string(),
55 description: "Target port for tunnel".to_string(),
56 required: true,
57 },
58 Parameter {
59 name: "status_code".to_string(),
60 type_hint: "number".to_string(),
61 description: "HTTP status code from proxy".to_string(),
62 required: true,
63 },
64 ])
65});
66
67pub static HTTP_PROXY_RESPONSE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
69 EventType::new(
70 "http_proxy_response_received",
71 "HTTP response received via proxy tunnel",
72 json!({
73 "type": "send_data",
74 "data_hex": "48656c6c6f"
75 }),
76 )
77 .with_parameters(vec![
78 Parameter {
79 name: "data_hex".to_string(),
80 type_hint: "string".to_string(),
81 description: "Response data (as hex string)".to_string(),
82 required: true,
83 },
84 Parameter {
85 name: "data_length".to_string(),
86 type_hint: "number".to_string(),
87 description: "Length of response data in bytes".to_string(),
88 required: true,
89 },
90 ])
91});
92
93#[derive(Default)]
95pub struct HttpProxyClientProtocol;
96
97impl HttpProxyClientProtocol {
98 pub fn new() -> Self {
99 Self
100 }
101}
102
103impl Protocol for HttpProxyClientProtocol {
105 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
106 vec![
107 ParameterDefinition {
108 name: "proxy_auth".to_string(),
109 description: "Proxy authentication credentials (username:password)".to_string(),
110 type_hint: "string".to_string(),
111 required: false,
112 example: json!("username:password"),
113 default: None,
114 },
115 ParameterDefinition {
116 name: "default_target".to_string(),
117 description: "Default target host:port for CONNECT tunnel".to_string(),
118 type_hint: "string".to_string(),
119 required: false,
120 example: json!("example.com:80"),
121 default: None,
122 },
123 ]
124 }
125 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
126 vec![
127 ActionDefinition {
128 name: "establish_tunnel".to_string(),
129 description: "Establish HTTP CONNECT tunnel to target".to_string(),
130 parameters: vec![
131 Parameter {
132 name: "target_host".to_string(),
133 type_hint: "string".to_string(),
134 description: "Target hostname or IP".to_string(),
135 required: true,
136 },
137 Parameter {
138 name: "target_port".to_string(),
139 type_hint: "number".to_string(),
140 description: "Target port".to_string(),
141 required: true,
142 },
143 ],
144 example: json!({
145 "type": "establish_tunnel",
146 "target_host": "example.com",
147 "target_port": 443
148 }),
149 log_template: None,
150 },
151 ActionDefinition {
152 name: "send_http_request".to_string(),
153 description: "Send HTTP request via established tunnel".to_string(),
154 parameters: vec![
155 Parameter {
156 name: "method".to_string(),
157 type_hint: "string".to_string(),
158 description: "HTTP method (GET, POST, etc.)".to_string(),
159 required: true,
160 },
161 Parameter {
162 name: "path".to_string(),
163 type_hint: "string".to_string(),
164 description: "Request path".to_string(),
165 required: true,
166 },
167 Parameter {
168 name: "headers".to_string(),
169 type_hint: "object".to_string(),
170 description: "HTTP headers".to_string(),
171 required: false,
172 },
173 Parameter {
174 name: "body".to_string(),
175 type_hint: "string".to_string(),
176 description: "Request body".to_string(),
177 required: false,
178 },
179 ],
180 example: json!({
181 "type": "send_http_request",
182 "method": "GET",
183 "path": "/",
184 "headers": {
185 "Host": "example.com",
186 "User-Agent": "NetGet/1.0"
187 }
188 }),
189 log_template: None,
190 },
191 ActionDefinition {
192 name: "send_data".to_string(),
193 description: "Send raw data through the proxy tunnel".to_string(),
194 parameters: vec![Parameter {
195 name: "data_hex".to_string(),
196 type_hint: "string".to_string(),
197 description: "Hexadecimal encoded data to send".to_string(),
198 required: true,
199 }],
200 example: json!({
201 "type": "send_data",
202 "data_hex": "48656c6c6f"
203 }),
204 log_template: None,
205 },
206 ActionDefinition {
207 name: "disconnect".to_string(),
208 description: "Disconnect from the proxy server".to_string(),
209 parameters: vec![],
210 example: json!({
211 "type": "disconnect"
212 }),
213 log_template: None,
214 },
215 ]
216 }
217 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
218 vec![
219 ActionDefinition {
220 name: "send_http_request".to_string(),
221 description: "Send HTTP request in response to received data".to_string(),
222 parameters: vec![
223 Parameter {
224 name: "method".to_string(),
225 type_hint: "string".to_string(),
226 description: "HTTP method".to_string(),
227 required: true,
228 },
229 Parameter {
230 name: "path".to_string(),
231 type_hint: "string".to_string(),
232 description: "Request path".to_string(),
233 required: true,
234 },
235 Parameter {
236 name: "headers".to_string(),
237 type_hint: "object".to_string(),
238 description: "HTTP headers".to_string(),
239 required: false,
240 },
241 Parameter {
242 name: "body".to_string(),
243 type_hint: "string".to_string(),
244 description: "Request body".to_string(),
245 required: false,
246 },
247 ],
248 example: json!({
249 "type": "send_http_request",
250 "method": "POST",
251 "path": "/api/data",
252 "body": "{\"key\": \"value\"}"
253 }),
254 log_template: None,
255 },
256 ActionDefinition {
257 name: "send_data".to_string(),
258 description: "Send raw data in response to received data".to_string(),
259 parameters: vec![Parameter {
260 name: "data_hex".to_string(),
261 type_hint: "string".to_string(),
262 description: "Hexadecimal encoded data to send".to_string(),
263 required: true,
264 }],
265 example: json!({
266 "type": "send_data",
267 "data_hex": "48656c6c6f"
268 }),
269 log_template: None,
270 },
271 ActionDefinition {
272 name: "wait_for_more".to_string(),
273 description: "Wait for more data before responding".to_string(),
274 parameters: vec![],
275 example: json!({
276 "type": "wait_for_more"
277 }),
278 log_template: None,
279 },
280 ]
281 }
282 fn protocol_name(&self) -> &'static str {
283 "HTTP Proxy"
284 }
285 fn get_event_types(&self) -> Vec<EventType> {
286 vec![
287 EventType::new(
288 "http_proxy_connected",
289 "Triggered when HTTP proxy client connects to proxy server",
290 json!({"type": "placeholder", "event_id": "http_proxy_connected"}),
291 ),
292 EventType::new(
293 "http_proxy_tunnel_established",
294 "Triggered when HTTP CONNECT tunnel is established",
295 json!({"type": "placeholder", "event_id": "http_proxy_tunnel_established"}),
296 ),
297 EventType::new(
298 "http_proxy_response_received",
299 "Triggered when data is received via proxy tunnel",
300 json!({"type": "placeholder", "event_id": "http_proxy_response_received"}),
301 ),
302 ]
303 }
304 fn stack_name(&self) -> &'static str {
305 "ETH>IP>TCP>HTTP"
306 }
307 fn keywords(&self) -> Vec<&'static str> {
308 vec![
309 "http proxy",
310 "http proxy client",
311 "connect via proxy",
312 "proxy",
313 ]
314 }
315 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
316 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
317
318 ProtocolMetadataV2::builder()
319 .state(DevelopmentState::Experimental)
320 .implementation("Tokio TcpStream with HTTP CONNECT protocol")
321 .llm_control("Full control over tunnel establishment and data transmission")
322 .e2e_testing("Squid proxy or tinyproxy as test proxy")
323 .build()
324 }
325 fn description(&self) -> &'static str {
326 "HTTP proxy client for tunneling connections through HTTP proxies"
327 }
328 fn example_prompt(&self) -> &'static str {
329 "Connect via HTTP proxy at localhost:8080 to reach example.com:443"
330 }
331 fn group_name(&self) -> &'static str {
332 "Proxy"
333 }
334 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
335 use crate::llm::actions::StartupExamples;
336 use serde_json::json;
337
338 StartupExamples::new(
339 json!({
341 "type": "open_client",
342 "remote_addr": "localhost:8080",
343 "base_stack": "http_proxy",
344 "instruction": "Establish tunnel to example.com:443 and send a GET request"
345 }),
346 json!({
348 "type": "open_client",
349 "remote_addr": "localhost:8080",
350 "base_stack": "http_proxy",
351 "event_handlers": [{
352 "event_pattern": "http_proxy_response_received",
353 "handler": {
354 "type": "script",
355 "language": "python",
356 "code": "<http_proxy_client_handler>"
357 }
358 }]
359 }),
360 json!({
362 "type": "open_client",
363 "remote_addr": "localhost:8080",
364 "base_stack": "http_proxy",
365 "event_handlers": [
366 {
367 "event_pattern": "http_proxy_connected",
368 "handler": {
369 "type": "static",
370 "actions": [{
371 "type": "establish_tunnel",
372 "target_host": "example.com",
373 "target_port": 80
374 }]
375 }
376 },
377 {
378 "event_pattern": "http_proxy_tunnel_established",
379 "handler": {
380 "type": "static",
381 "actions": [{
382 "type": "send_http_request",
383 "method": "GET",
384 "path": "/",
385 "headers": {"Host": "example.com"}
386 }]
387 }
388 }
389 ]
390 }),
391 )
392 }
393}
394
395impl Client for HttpProxyClientProtocol {
397 fn connect(
398 &self,
399 ctx: crate::protocol::ConnectContext,
400 ) -> std::pin::Pin<
401 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
402 > {
403 Box::pin(async move {
404 use crate::client::http_proxy::HttpProxyClient;
405 HttpProxyClient::connect_with_llm_actions(
406 ctx.remote_addr,
407 ctx.llm_client,
408 ctx.state,
409 ctx.status_tx,
410 ctx.client_id,
411 ctx.startup_params,
412 )
413 .await
414 })
415 }
416 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
417 let action_type = action
418 .get("type")
419 .and_then(|v| v.as_str())
420 .context("Missing 'type' field in action")?;
421
422 match action_type {
423 "establish_tunnel" => {
424 let target_host = action
425 .get("target_host")
426 .and_then(|v| v.as_str())
427 .context("Missing 'target_host' field")?
428 .to_string();
429
430 let raw = action
437 .get("target_port")
438 .and_then(|v| v.as_u64())
439 .context("Missing 'target_port' field")?;
440 if !(1..=65535).contains(&raw) {
441 return Err(anyhow::anyhow!(
442 "'target_port' {raw} is not a TCP port (1-65535). Truncating it \
443 would tunnel to a different service than the one named."
444 ));
445 }
446 let target_port = raw as u16;
447
448 Ok(ClientActionResult::Custom {
449 name: "establish_tunnel".to_string(),
450 data: json!({
451 "target_host": target_host,
452 "target_port": target_port,
453 }),
454 })
455 }
456 "send_http_request" => {
457 let method = action
458 .get("method")
459 .and_then(|v| v.as_str())
460 .context("Missing 'method' field")?
461 .to_string();
462
463 let path = action
464 .get("path")
465 .and_then(|v| v.as_str())
466 .context("Missing 'path' field")?
467 .to_string();
468
469 let headers = action.get("headers").and_then(|v| v.as_object()).cloned();
470
471 let body = action
472 .get("body")
473 .and_then(|v| v.as_str())
474 .map(|s| s.to_string());
475
476 let mut request = format!("{} {} HTTP/1.1\r\n", method, path);
478
479 if let Some(hdrs) = headers {
481 for (key, value) in hdrs {
482 if let Some(val_str) = value.as_str() {
483 request.push_str(&format!("{}: {}\r\n", key, val_str));
484 }
485 }
486 }
487
488 request.push_str("\r\n");
490
491 if let Some(body_str) = body {
493 request.push_str(&body_str);
494 }
495
496 Ok(ClientActionResult::SendData(request.into_bytes()))
497 }
498 "send_data" => {
499 let data_hex = action
500 .get("data_hex")
501 .and_then(|v| v.as_str())
502 .context("Missing 'data_hex' field")?;
503
504 let data = hex::decode(data_hex).context("Invalid hex data")?;
505
506 Ok(ClientActionResult::SendData(data))
507 }
508 "disconnect" => Ok(ClientActionResult::Disconnect),
509 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
510 _ => Err(anyhow::anyhow!(
511 "Unknown HTTP proxy client action: {}",
512 action_type
513 )),
514 }
515 }
516}