Seregon/zftpd

Zero-copy FTP/HTTP Daemon compatible with all POSIX systems

C/11.0 KB/No license
src/ftp_protocol.c
zftpd / src / ftp_protocol.c
1/*
2MIT License
3 
4Copyright (c) 2026 Seregon
5 
6Permission is hereby granted, free of charge, to any person obtaining a copy
7of this software and associated documentation files (the "Software"), to deal
8in the Software without restriction, including without limitation the rights
9to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10copies of the Software, and to permit persons to whom the Software is
11furnished to do so, subject to the following conditions:
12 
13The above copyright notice and this permission notice shall be included in all
14copies or substantial portions of the Software.
15 
16THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22SOFTWARE.
23*/
24 
25/**
26 * @file ftp_protocol.c
27 * @brief FTP protocol parsing and command dispatch implementation
28 *
29 * @author SeregonWar
30 * @version 1.0.0
31 * @date 2026-02-13
32 *
33 */
34 
35#include "ftp_protocol.h"
36#include "ftp_commands.h"
37#include <ctype.h>
38#include <stdio.h>
39#include <string.h>
40#include <strings.h>
41 
42/*===========================================================================*
43 * COMMAND TABLE
44 *===========================================================================*/
45 
46/**
47 * FTP command lookup table
48 *
49 * DESIGN: Sorted alphabetically for future binary search optimization
50 * NOTE: All command names MUST be uppercase
51 */
52static const ftp_command_entry_t command_table[] = {
53 /* Authentication and control */
54 {"USER", cmd_USER, FTP_ARGS_REQUIRED},
55 {"PASS", cmd_PASS, FTP_ARGS_OPTIONAL},
56 {"QUIT", cmd_QUIT, FTP_ARGS_NONE},
57 {"NOOP", cmd_NOOP, FTP_ARGS_NONE},
58 
59 /* Navigation */
60 {"CWD", cmd_CWD, FTP_ARGS_REQUIRED},
61 {"CDUP", cmd_CDUP, FTP_ARGS_NONE},
62 {"PWD", cmd_PWD, FTP_ARGS_NONE},
63 
64 /* Directory listing */
65 {"LIST", cmd_LIST, FTP_ARGS_OPTIONAL},
66 {"NLST", cmd_NLST, FTP_ARGS_OPTIONAL},
67#if FTP_ENABLE_MLST
68 {"MLSD", cmd_MLSD, FTP_ARGS_OPTIONAL},
69 {"MLST", cmd_MLST, FTP_ARGS_OPTIONAL},
70#endif
71 
72 /* File transfer */
73 {"RETR", cmd_RETR, FTP_ARGS_REQUIRED},
74 {"STOR", cmd_STOR, FTP_ARGS_REQUIRED},
75 {"APPE", cmd_APPE, FTP_ARGS_REQUIRED},
76#if FTP_ENABLE_REST
77 {"REST", cmd_REST, FTP_ARGS_REQUIRED},
78#endif
79 
80 /* File management */
81 {"DELE", cmd_DELE, FTP_ARGS_REQUIRED},
82 {"RMD", cmd_RMD, FTP_ARGS_REQUIRED},
83 {"MKD", cmd_MKD, FTP_ARGS_REQUIRED},
84 {"RNFR", cmd_RNFR, FTP_ARGS_REQUIRED},
85 {"RNTO", cmd_RNTO, FTP_ARGS_REQUIRED},
86 
87 /* Async Copy */
88 {"CPFR", cmd_CPFR, FTP_ARGS_REQUIRED},
89 {"CPTO", cmd_CPTO, FTP_ARGS_REQUIRED},
90 {"COPY", cmd_COPY, FTP_ARGS_REQUIRED},
91 
92 /* Data connection */
93 {"PORT", cmd_PORT, FTP_ARGS_REQUIRED},
94 {"PASV", cmd_PASV, FTP_ARGS_NONE},
95 {"EPSV", cmd_EPSV, FTP_ARGS_OPTIONAL},
96 
97 /* Feature negotiation */
98 {"OPTS", cmd_OPTS, FTP_ARGS_REQUIRED},
99 {"SITE", cmd_SITE, FTP_ARGS_REQUIRED},
100 {"CLNT", cmd_CLNT, FTP_ARGS_OPTIONAL},
101 
102/* Information */
103#if FTP_ENABLE_SIZE
104 {"SIZE", cmd_SIZE, FTP_ARGS_REQUIRED},
105#endif
106#if FTP_ENABLE_MDTM
107 {"MDTM", cmd_MDTM, FTP_ARGS_REQUIRED},
108#endif
109 {"STAT", cmd_STAT, FTP_ARGS_OPTIONAL},
110 {"SYST", cmd_SYST, FTP_ARGS_NONE},
111 {"FEAT", cmd_FEAT, FTP_ARGS_NONE},
112 {"HELP", cmd_HELP, FTP_ARGS_OPTIONAL},
113 
114 /* Transfer parameters */
115 {"TYPE", cmd_TYPE, FTP_ARGS_REQUIRED},
116 {"MODE", cmd_MODE, FTP_ARGS_REQUIRED},
117 {"STRU", cmd_STRU, FTP_ARGS_REQUIRED},
118 
119/* Encryption */
120#if FTP_ENABLE_CRYPTO
121 {"AUTH", cmd_AUTH, FTP_ARGS_REQUIRED},
122#endif
123};
124 
125static const size_t command_table_size =
126 sizeof(command_table) / sizeof(command_table[0]);
127 
128/*===========================================================================*
129 * DEFAULT REPLY MESSAGES
130 *===========================================================================*/
131 
132/**
133 * Default reply messages for standard codes
134 */
135static const char *get_default_message(ftp_reply_code_t code) {
136 switch (code) {
137 /* 1xx - Positive Preliminary */
138 case FTP_REPLY_150_FILE_OK:
139 return "File status okay; about to open data connection.";
140 
141 /* 2xx - Positive Completion */
142 case FTP_REPLY_200_OK:
143 return "Command okay.";
144 case FTP_REPLY_211_SYSTEM_STATUS:
145 return "System status.";
146 case FTP_REPLY_214_HELP:
147 return "Help message.";
148 case FTP_REPLY_215_SYSTEM_TYPE:
149 return "UNIX Type: L8";
150 case FTP_REPLY_220_SERVICE_READY:
151 return "Service ready for new user.";
152 case FTP_REPLY_221_GOODBYE:
153 return "Service closing control connection.";
154 case FTP_REPLY_227_PASV_MODE:
155 return "Entering Passive Mode.";
156 case FTP_REPLY_229_EPSV_MODE:
157 return "Entering Extended Passive Mode.";
158 case FTP_REPLY_225_DATA_OPEN:
159 return "Data connection open; no transfer in progress.";
160 case FTP_REPLY_226_TRANSFER_COMPLETE:
161 return "Closing data connection. Transfer complete.";
162 case FTP_REPLY_230_LOGGED_IN:
163 return "User logged in, proceed.";
164 case FTP_REPLY_250_FILE_ACTION_OK:
165 return "Requested file action okay, completed.";
166 
167 /* 3xx - Positive Intermediate */
168 case FTP_REPLY_331_NEED_PASSWORD:
169 return "User name okay, need password.";
170 case FTP_REPLY_350_PENDING:
171 return "Requested file action pending further information.";
172 
173 /* 4xx - Transient Negative */
174 case FTP_REPLY_421_SERVICE_UNAVAIL:
175 return "Service not available, closing control connection.";
176 case FTP_REPLY_425_CANT_OPEN_DATA:
177 return "Can't open data connection.";
178 case FTP_REPLY_426_TRANSFER_ABORTED:
179 return "Connection closed; transfer aborted.";
180 case FTP_REPLY_450_FILE_UNAVAILABLE:
181 return "Requested file action not taken.";
182 case FTP_REPLY_451_LOCAL_ERROR:
183 return "Requested action aborted: local error.";
184 
185 /* 5xx - Permanent Negative */
186 case FTP_REPLY_500_SYNTAX_ERROR:
187 return "Syntax error, command unrecognized.";
188 case FTP_REPLY_501_SYNTAX_ARGS:
189 return "Syntax error in parameters or arguments.";
190 case FTP_REPLY_502_NOT_IMPLEMENTED:
191 return "Command not implemented.";
192 case FTP_REPLY_503_BAD_SEQUENCE:
193 return "Bad sequence of commands.";
194 case FTP_REPLY_530_NOT_LOGGED_IN:
195 return "Not logged in.";
196 case FTP_REPLY_550_FILE_ERROR:
197 return "Requested action not taken. File unavailable.";
198 case FTP_REPLY_553_FILENAME_INVALID:
199 return "Requested action not taken. File name not allowed.";
200 
201 default:
202 return "Unknown reply code.";
203 }
204}
205 
206/*===========================================================================*
207 * COMMAND PARSING
208 *===========================================================================*/
209 
210/**
211 * @brief Parse FTP command line
212 */
213ftp_error_t ftp_parse_command_line(const char *line, char *command, char *args,
214 size_t cmd_size, size_t args_size) {
215 /* Validate parameters */
216 if ((line == NULL) || (command == NULL)) {
217 return FTP_ERR_INVALID_PARAM;
218 }
219 
220 if (cmd_size == 0U) {
221 return FTP_ERR_INVALID_PARAM;
222 }
223 
224 /* Find first space (separator between command and args) */
225 const char *space = strchr(line, ' ');
226 size_t cmd_len;
227 
228 if (space == NULL) {
229 /* No arguments: entire line is command */
230 cmd_len = strlen(line);
231 } else {
232 /* Command ends at space */
233 cmd_len = (size_t)(space - line);
234 }
235 
236 /* Validate command length */
237 if ((cmd_len == 0U) || (cmd_len >= cmd_size)) {
238 return FTP_ERR_PROTOCOL;
239 }
240 
241 /* Copy command and convert to uppercase */
242 for (size_t i = 0U; i < cmd_len; i++) {
243 command[i] = (char)toupper((unsigned char)line[i]);
244 }
245 command[cmd_len] = '\0';
246 
247 /* Extract arguments if present */
248 if ((space != NULL) && (args != NULL)) {
249 /* Skip leading whitespace */
250 const char *arg_start = space + 1;
251 while ((*arg_start != '\0') && isspace((unsigned char)*arg_start)) {
252 arg_start++;
253 }
254 
255 /* Copy arguments */
256 size_t arg_len = strlen(arg_start);
257 
258 /* Trim trailing whitespace */
259 while ((arg_len > 0U) && isspace((unsigned char)arg_start[arg_len - 1U])) {
260 arg_len--;
261 }
262 
263 if (arg_len >= args_size) {
264 return FTP_ERR_PROTOCOL; /* Arguments too long */
265 }
266 
267 if (arg_len > 0U) {
268 memcpy(args, arg_start, arg_len);
269 args[arg_len] = '\0';
270 } else {
271 args[0] = '\0'; /* Empty arguments */
272 }
273 } else if (args != NULL) {
274 args[0] = '\0';
275 }
276 
277 return FTP_OK;
278}
279 
280/**
281 * @brief Find command in lookup table
282 */
283const ftp_command_entry_t *ftp_find_command(const char *name) {
284 if (name == NULL) {
285 return NULL;
286 }
287 
288 /* Linear search (table is small, ~30 entries) */
289 for (size_t i = 0U; i < command_table_size; i++) {
290 if (strcasecmp(name, command_table[i].name) == 0) {
291 return &command_table[i];
292 }
293 }
294 
295 return NULL; /* Command not found */
296}
297 
298/**
299 * @brief Validate command arguments
300 */
301ftp_error_t ftp_validate_command_args(const ftp_command_entry_t *cmd,
302 const char *args) {
303 if (cmd == NULL) {
304 return FTP_ERR_INVALID_PARAM;
305 }
306 
307 int has_args = (args != NULL) && (args[0] != '\0');
308 
309 switch (cmd->args_req) {
310 case FTP_ARGS_NONE:
311 if (has_args) {
312 return FTP_ERR_PROTOCOL; /* Args not allowed */
313 }
314 break;
315 
316 case FTP_ARGS_REQUIRED:
317 if (!has_args) {
318 return FTP_ERR_PROTOCOL; /* Args required */
319 }
320 break;
321 
322 case FTP_ARGS_OPTIONAL:
323 /* Both cases allowed */
324 break;
325 
326 default:
327 return FTP_ERR_PROTOCOL; /* Invalid args_req value */
328 }
329 
330 return FTP_OK;
331}
332 
333/**
334 * @brief Get command table
335 */
336const ftp_command_entry_t *ftp_get_command_table(size_t *count) {
337 if (count != NULL) {
338 *count = command_table_size;
339 }
340 
341 return command_table;
342}
343 
344/*===========================================================================*
345 * REPLY FORMATTING
346 *===========================================================================*/
347 
348/**
349 * @brief Format FTP reply
350 */
351ssize_t ftp_format_reply(ftp_reply_code_t code, const char *message,
352 char *buffer, size_t size) {
353 /* Validate parameters */
354 if (buffer == NULL) {
355 return FTP_ERR_INVALID_PARAM;
356 }
357 
358 if (size < 64U) {
359 return FTP_ERR_INVALID_PARAM; /* Buffer too small */
360 }
361 
362 /* Use default message if none provided */
363 const char *msg = message;
364 if (msg == NULL) {
365 msg = get_default_message(code);
366 }
367 
368 /* Format: "CODE Message\r\n" */
369 int n = snprintf(buffer, size, "%d %s\r\n", (int)code, msg);
370 
371 if ((n < 0) || ((size_t)n >= size)) {
372 return FTP_ERR_INVALID_PARAM; /* Format error or truncation */
373 }
374 
375 return (ssize_t)n;
376}
377 
378/**
379 * @brief Get default reply message
380 */
381const char *ftp_get_default_reply_message(ftp_reply_code_t code) {
382 return get_default_message(code);
383}
384