Skip to main content

netget/client/ftp/
actions.rs

1//! FTP client protocol actions implementation
2//!
3//! Implements FTP client actions for LLM-controlled FTP connections.
4
5use crate::llm::actions::{
6    client_trait::{Client, ClientActionResult},
7    protocol_trait::Protocol,
8    ActionDefinition, Parameter,
9};
10use crate::protocol::EventType;
11use crate::state::app_state::AppState;
12use anyhow::{Context, Result};
13use serde_json::json;
14use std::sync::LazyLock;
15
16/// FTP client connected event
17pub static FTP_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
18    EventType::new(
19        "ftp_connected",
20        "FTP client successfully connected to server",
21        json!({"type": "wait_for_more"}),
22    )
23    .with_parameters(vec![Parameter {
24        name: "remote_addr".to_string(),
25        type_hint: "string".to_string(),
26        description: "Remote FTP server address".to_string(),
27        required: true,
28    }])
29});
30
31/// FTP client response received event
32pub static FTP_CLIENT_RESPONSE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
33    EventType::new(
34        "ftp_response",
35        "Response received from FTP server",
36        json!({"type": "send_ftp_command", "command": "USER anonymous"}),
37    )
38    .with_parameters(vec![
39        Parameter {
40            name: "response".to_string(),
41            type_hint: "string".to_string(),
42            description: "The full response line from the server".to_string(),
43            required: true,
44        },
45        Parameter {
46            name: "response_code".to_string(),
47            type_hint: "number".to_string(),
48            description: "The FTP response code (e.g., 220, 230, 550)".to_string(),
49            required: false,
50        },
51    ])
52});
53
54/// FTP client protocol action handler
55pub struct FtpClientProtocol;
56
57impl Default for FtpClientProtocol {
58    fn default() -> Self {
59        Self::new()
60    }
61}
62
63impl FtpClientProtocol {
64    pub fn new() -> Self {
65        Self
66    }
67}
68
69// Implement Protocol trait (common functionality)
70impl Protocol for FtpClientProtocol {
71    fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
72        vec![
73            ActionDefinition {
74                name: "send_ftp_command".to_string(),
75                description: "Send an FTP command to the server".to_string(),
76                parameters: vec![Parameter {
77                    name: "command".to_string(),
78                    type_hint: "string".to_string(),
79                    description: "FTP command (e.g., 'USER anonymous', 'LIST', 'QUIT')".to_string(),
80                    required: true,
81                }],
82                example: json!({
83                    "type": "send_ftp_command",
84                    "command": "USER anonymous"
85                }),
86                log_template: None,
87            },
88            ActionDefinition {
89                name: "disconnect".to_string(),
90                description: "Disconnect from the FTP server".to_string(),
91                parameters: vec![],
92                example: json!({
93                    "type": "disconnect"
94                }),
95                log_template: None,
96            },
97        ]
98    }
99
100    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
101        vec![
102            ActionDefinition {
103                name: "send_ftp_command".to_string(),
104                description: "Send FTP command in response to server message".to_string(),
105                parameters: vec![Parameter {
106                    name: "command".to_string(),
107                    type_hint: "string".to_string(),
108                    description: "FTP command to send".to_string(),
109                    required: true,
110                }],
111                example: json!({
112                    "type": "send_ftp_command",
113                    "command": "PASS guest@example.com"
114                }),
115                log_template: None,
116            },
117            ActionDefinition {
118                name: "wait_for_more".to_string(),
119                description: "Wait for more data from the server before responding".to_string(),
120                parameters: vec![],
121                example: json!({
122                    "type": "wait_for_more"
123                }),
124                log_template: None,
125            },
126        ]
127    }
128
129    fn protocol_name(&self) -> &'static str {
130        "FTP"
131    }
132
133    fn get_event_types(&self) -> Vec<EventType> {
134        vec![
135            FTP_CLIENT_CONNECTED_EVENT.clone(),
136            FTP_CLIENT_RESPONSE_EVENT.clone(),
137        ]
138    }
139
140    fn stack_name(&self) -> &'static str {
141        "ETH>IP>TCP>FTP"
142    }
143
144    fn keywords(&self) -> Vec<&'static str> {
145        vec!["ftp client", "connect to ftp", "ftp connect"]
146    }
147
148    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
149        use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
150
151        ProtocolMetadataV2::builder()
152            .state(DevelopmentState::Experimental)
153            .implementation("Tokio TcpStream with line-based FTP protocol")
154            .llm_control("Full control over FTP commands")
155            .e2e_testing("Local FTP server (vsftpd, netget FTP server)")
156            .build()
157    }
158
159    fn description(&self) -> &'static str {
160        "FTP client for connecting to FTP servers"
161    }
162
163    fn example_prompt(&self) -> &'static str {
164        "Connect to ftp://localhost:21 and list files using anonymous login"
165    }
166
167    fn group_name(&self) -> &'static str {
168        "Application"
169    }
170    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
171        use crate::llm::actions::StartupExamples;
172        use serde_json::json;
173
174        StartupExamples::new(
175            // LLM mode: LLM controls FTP client interactions
176            json!({
177                "type": "open_client",
178                "remote_addr": "ftp.example.com:21",
179                "base_stack": "ftp",
180                "instruction": "Login anonymously and list the root directory files"
181            }),
182            // Script mode: Code-based deterministic responses
183            json!({
184                "type": "open_client",
185                "remote_addr": "ftp.example.com:21",
186                "base_stack": "ftp",
187                "event_handlers": [{
188                    "event_pattern": "ftp_response",
189                    "handler": {
190                        "type": "script",
191                        "language": "python",
192                        "code": "<ftp_client_handler>"
193                    }
194                }]
195            }),
196            // Static mode: Fixed responses - login sequence
197            json!({
198                "type": "open_client",
199                "remote_addr": "ftp.example.com:21",
200                "base_stack": "ftp",
201                "event_handlers": [
202                    {
203                        "event_pattern": "ftp_connected",
204                        "handler": {
205                            "type": "static",
206                            "actions": [{
207                                "type": "wait_for_more"
208                            }]
209                        }
210                    },
211                    {
212                        "event_pattern": "ftp_response",
213                        "handler": {
214                            "type": "static",
215                            "actions": [{
216                                "type": "send_ftp_command",
217                                "command": "QUIT"
218                            }]
219                        }
220                    }
221                ]
222            }),
223        )
224    }
225}
226
227// Implement Client trait (client-specific functionality)
228impl Client for FtpClientProtocol {
229    fn connect(
230        &self,
231        ctx: crate::protocol::ConnectContext,
232    ) -> std::pin::Pin<
233        Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
234    > {
235        Box::pin(async move {
236            use crate::client::ftp::FtpClient;
237            FtpClient::connect_with_llm_actions(
238                ctx.remote_addr,
239                ctx.llm_client,
240                ctx.state,
241                ctx.status_tx,
242                ctx.client_id,
243            )
244            .await
245        })
246    }
247
248    fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
249        let action_type = action
250            .get("type")
251            .and_then(|v| v.as_str())
252            .context("Missing 'type' field in action")?;
253
254        match action_type {
255            "send_ftp_command" => {
256                let command = action
257                    .get("command")
258                    .and_then(|v| v.as_str())
259                    .context("Missing 'command' parameter in send_ftp_command action")?;
260
261                // FTP commands must end with CRLF
262                let data = if command.ends_with("\r\n") {
263                    command.as_bytes().to_vec()
264                } else if command.ends_with('\n') {
265                    format!("{}\r", command.trim_end_matches('\n')).into_bytes()
266                } else {
267                    format!("{}\r\n", command).into_bytes()
268                };
269
270                Ok(ClientActionResult::SendData(data))
271            }
272            "disconnect" => Ok(ClientActionResult::Disconnect),
273            "wait_for_more" => Ok(ClientActionResult::WaitForMore),
274            _ => Err(anyhow::anyhow!(
275                "Unknown FTP client action: {}",
276                action_type
277            )),
278        }
279    }
280}