1use 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
16pub 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
31pub 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
54pub 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
69impl 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 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 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 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
227impl 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 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}