Skip to main content

netget/client/tls/
actions.rs

1//! TLS 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/// TLS client connected event - emitted after successful TLS handshake
15pub static TLS_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16    EventType::new(
17        "tls_client_connected",
18        "TLS client successfully completed handshake with server",
19        json!({
20            "type": "send_tls_data",
21            "data": "Hello"
22        }),
23    )
24    .with_parameters(vec![
25        Parameter {
26            name: "remote_addr".to_string(),
27            type_hint: "string".to_string(),
28            description: "Remote TLS server address".to_string(),
29            required: true,
30        },
31        Parameter {
32            name: "server_name".to_string(),
33            type_hint: "string".to_string(),
34            description: "SNI server name used for TLS handshake".to_string(),
35            required: true,
36        },
37    ])
38});
39
40/// TLS client data received event - emitted when encrypted data is received
41pub static TLS_CLIENT_DATA_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
42    EventType::new(
43        "tls_client_data_received",
44        "Data received from TLS server (decrypted)",
45        json!({
46            "type": "send_tls_data",
47            "data": "Response"
48        }),
49    )
50    .with_parameters(vec![
51        Parameter {
52            name: "data".to_string(),
53            type_hint: "string".to_string(),
54            description: "The decrypted data received (UTF-8 text or hex for binary)".to_string(),
55            required: true,
56        },
57        Parameter {
58            name: "data_length".to_string(),
59            type_hint: "number".to_string(),
60            description: "Length of data in bytes".to_string(),
61            required: true,
62        },
63    ])
64});
65
66/// TLS client protocol action handler
67pub struct TlsClientProtocol;
68
69impl Default for TlsClientProtocol {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75impl TlsClientProtocol {
76    pub fn new() -> Self {
77        Self
78    }
79}
80
81// Implement Protocol trait (common functionality)
82impl Protocol for TlsClientProtocol {
83    fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
84        vec![
85            ParameterDefinition {
86                name: "accept_invalid_certs".to_string(),
87                type_hint: "boolean".to_string(),
88                description:
89                    "Accept self-signed or invalid certificates (useful for testing, default: false)"
90                        .to_string(),
91                required: false,
92                example: json!(false),
93                default: None,
94            },
95            ParameterDefinition {
96                name: "server_name".to_string(),
97                type_hint: "string".to_string(),
98                description: "SNI server name for TLS handshake (default: derived from remote_addr hostname)"
99                    .to_string(),
100                required: false,
101                example: json!("example.com"),
102                default: None,
103            },
104            ParameterDefinition {
105                name: "custom_ca_cert_pem".to_string(),
106                type_hint: "string".to_string(),
107                description: "Custom CA certificate in PEM format to use for validation (instead of system roots)"
108                    .to_string(),
109                required: false,
110                example: json!("-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"),
111                default: None,
112            },
113        ]
114    }
115
116    fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
117        vec![
118            ActionDefinition {
119                name: "send_tls_data".to_string(),
120                description: "Send data over TLS connection (UTF-8 string or hex-encoded)"
121                    .to_string(),
122                parameters: vec![
123                    Parameter {
124                        name: "data".to_string(),
125                        type_hint: "string".to_string(),
126                        description: "UTF-8 string data to send (preferred for text)".to_string(),
127                        required: false,
128                    },
129                    Parameter {
130                        name: "data_hex".to_string(),
131                        type_hint: "string".to_string(),
132                        description: "Hexadecimal encoded data to send (for binary data)"
133                            .to_string(),
134                        required: false,
135                    },
136                ],
137                example: json!({
138                    "type": "send_tls_data",
139                    "data": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
140                }),
141                log_template: None,
142            },
143            ActionDefinition {
144                name: "disconnect".to_string(),
145                description: "Disconnect from the TLS server".to_string(),
146                parameters: vec![],
147                example: json!({
148                    "type": "disconnect"
149                }),
150                log_template: None,
151            },
152        ]
153    }
154
155    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
156        vec![
157            ActionDefinition {
158                name: "send_tls_data".to_string(),
159                description:
160                    "Send TLS data in response to received data (UTF-8 string or hex-encoded)"
161                        .to_string(),
162                parameters: vec![
163                    Parameter {
164                        name: "data".to_string(),
165                        type_hint: "string".to_string(),
166                        description: "UTF-8 string data to send (preferred for text)".to_string(),
167                        required: false,
168                    },
169                    Parameter {
170                        name: "data_hex".to_string(),
171                        type_hint: "string".to_string(),
172                        description: "Hexadecimal encoded data to send (for binary data)"
173                            .to_string(),
174                        required: false,
175                    },
176                ],
177                example: json!({
178                    "type": "send_tls_data",
179                    "data": "Response data"
180                }),
181                log_template: None,
182            },
183            ActionDefinition {
184                name: "wait_for_more".to_string(),
185                description: "Wait for more data before responding".to_string(),
186                parameters: vec![],
187                example: json!({
188                    "type": "wait_for_more"
189                }),
190                log_template: None,
191            },
192        ]
193    }
194
195    fn protocol_name(&self) -> &'static str {
196        "TLS"
197    }
198
199    fn get_event_types(&self) -> Vec<EventType> {
200        vec![
201            EventType::new(
202                "tls_client_connected",
203                "Triggered when TLS client completes handshake with server",
204                json!({"type": "placeholder", "event_id": "tls_client_connected"}),
205            ),
206            EventType::new(
207                "tls_client_data_received",
208                "Triggered when TLS client receives decrypted data from server",
209                json!({"type": "placeholder", "event_id": "tls_client_data_received"}),
210            ),
211        ]
212    }
213
214    fn stack_name(&self) -> &'static str {
215        "ETH>IP>TCP>TLS"
216    }
217
218    fn keywords(&self) -> Vec<&'static str> {
219        vec!["tls", "ssl", "tls client", "connect to tls", "https"]
220    }
221
222    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
223        use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
224
225        ProtocolMetadataV2::builder()
226            .state(DevelopmentState::Experimental)
227            .implementation("tokio-rustls TLS client with certificate validation control")
228            .llm_control("Full control over encrypted data exchange")
229            .e2e_testing("NetGet TLS server for testing")
230            .notes("Generic TLS client for encrypted protocols - LLM implements application layer")
231            .build()
232    }
233
234    fn description(&self) -> &'static str {
235        "TLS client for connecting to TLS/SSL servers"
236    }
237
238    fn example_prompt(&self) -> &'static str {
239        "Connect to TLS at localhost:8443 and send an HTTP request"
240    }
241
242    fn group_name(&self) -> &'static str {
243        "Core"
244    }
245
246    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
247        use crate::llm::actions::StartupExamples;
248        use serde_json::json;
249
250        StartupExamples::new(
251            // LLM mode: LLM handles TLS client
252            json!({
253                "type": "open_client",
254                "remote_addr": "example.com:443",
255                "base_stack": "tls",
256                "instruction": "Send an HTTPS GET request and display the response"
257            }),
258            // Script mode: Code-based TLS handling
259            json!({
260                "type": "open_client",
261                "remote_addr": "example.com:443",
262                "base_stack": "tls",
263                "event_handlers": [{
264                    "event_pattern": "tls_client_data_received",
265                    "handler": {
266                        "type": "script",
267                        "language": "python",
268                        "code": "<tls_handler>"
269                    }
270                }]
271            }),
272            // Static mode: Fixed TLS request
273            json!({
274                "type": "open_client",
275                "remote_addr": "example.com:443",
276                "base_stack": "tls",
277                "event_handlers": [{
278                    "event_pattern": "tls_client_connected",
279                    "handler": {
280                        "type": "static",
281                        "actions": [{
282                            "type": "send_tls_data",
283                            "data": "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n"
284                        }]
285                    }
286                }]
287            }),
288        )
289    }
290}
291
292// Implement Client trait (client-specific functionality)
293impl Client for TlsClientProtocol {
294    fn connect(
295        &self,
296        ctx: crate::protocol::ConnectContext,
297    ) -> std::pin::Pin<
298        Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
299    > {
300        Box::pin(async move {
301            use crate::client::tls::TlsClient;
302            TlsClient::connect_with_llm_actions(
303                ctx.remote_addr,
304                ctx.llm_client,
305                ctx.state,
306                ctx.status_tx,
307                ctx.client_id,
308                ctx.startup_params,
309            )
310            .await
311        })
312    }
313
314    fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
315        let action_type = action
316            .get("type")
317            .and_then(|v| v.as_str())
318            .context("Missing 'type' field in action")?;
319
320        match action_type {
321            "send_tls_data" => {
322                // Support both UTF-8 string (data) and hex-encoded (data_hex)
323                // Prefer UTF-8 for easier LLM interaction
324                let data = if let Some(utf8_data) = action.get("data").and_then(|v| v.as_str()) {
325                    // UTF-8 string provided
326                    utf8_data.as_bytes().to_vec()
327                } else if let Some(hex_data) = action.get("data_hex").and_then(|v| v.as_str()) {
328                    // Hex string provided
329                    hex::decode(hex_data).context("Invalid hex data in data_hex field")?
330                } else {
331                    return Err(anyhow::anyhow!(
332                        "Missing 'data' or 'data_hex' field in send_tls_data action"
333                    ));
334                };
335
336                Ok(ClientActionResult::SendData(data))
337            }
338            "disconnect" => Ok(ClientActionResult::Disconnect),
339            "wait_for_more" => Ok(ClientActionResult::WaitForMore),
340            _ => Err(anyhow::anyhow!(
341                "Unknown TLS client action: {}",
342                action_type
343            )),
344        }
345    }
346}