Skip to main content

netget/client/http_proxy/
actions.rs

1//! HTTP proxy client protocol actions implementation
2
3use 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
14/// HTTP proxy client connected event
15pub 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
33/// HTTP proxy tunnel established event
34pub 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
67/// HTTP proxy response received event
68pub 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/// HTTP proxy client protocol action handler
94#[derive(Default)]
95pub struct HttpProxyClientProtocol;
96
97impl HttpProxyClientProtocol {
98    pub fn new() -> Self {
99        Self
100    }
101}
102
103// Implement Protocol trait (common functionality)
104impl 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            // LLM mode: LLM controls HTTP proxy tunnel
340            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            // Script mode: Code-based proxy handling
347            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            // Static mode: Fixed tunnel establishment
361            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
395// Implement Client trait (client-specific functionality)
396impl 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                // Range-checked, not narrowed. `as u16` wraps in silence, so
431                // `target_port: 66079` became 543 and the CONNECT went to a *different
432                // service on the same host* than the one the model named — quietly, with
433                // every log line downstream reporting the port it ended up at rather than
434                // the one it was asked for. A client that loses its target must fail rather
435                // than pick one.
436                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                // Build HTTP request string
477                let mut request = format!("{} {} HTTP/1.1\r\n", method, path);
478
479                // Add headers
480                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                // End headers
489                request.push_str("\r\n");
490
491                // Add body if present
492                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}