diff options
author | Denis Vlasenko <vda.linux@googlemail.com> | 2009-03-08 09:31:28 +0000 |
---|---|---|
committer | Denis Vlasenko <vda.linux@googlemail.com> | 2009-03-08 09:31:28 +0000 |
commit | bf9d17949ee8b5ba8bbfdcd9a78b9a12fb122b63 (patch) | |
tree | d8f4283dffecd085d5316d9d349815aa73696cee /networking/ftpd.c | |
parent | 16b4a2de97d98d9683909b7b083035e65b3f87de (diff) | |
download | busybox-w32-bf9d17949ee8b5ba8bbfdcd9a78b9a12fb122b63.tar.gz busybox-w32-bf9d17949ee8b5ba8bbfdcd9a78b9a12fb122b63.tar.bz2 busybox-w32-bf9d17949ee8b5ba8bbfdcd9a78b9a12fb122b63.zip |
adding forgotten new file
Diffstat (limited to 'networking/ftpd.c')
-rw-r--r-- | networking/ftpd.c | 1130 |
1 files changed, 1130 insertions, 0 deletions
diff --git a/networking/ftpd.c b/networking/ftpd.c new file mode 100644 index 000000000..ab7308be9 --- /dev/null +++ b/networking/ftpd.c | |||
@@ -0,0 +1,1130 @@ | |||
1 | /* vi: set sw=4 ts=4: */ | ||
2 | /* | ||
3 | * Simple FTP daemon, based on vsftpd 2.0.7 (written by Chris Evans) | ||
4 | * | ||
5 | * Author: Adam Tkac <vonsch@gmail.com> | ||
6 | * | ||
7 | * Only subset of FTP protocol is implemented but vast majority of clients | ||
8 | * should not have any problem. You have to run this daemon via inetd. | ||
9 | * | ||
10 | * Options: | ||
11 | * -w - enable FTP write commands | ||
12 | */ | ||
13 | |||
14 | #include "libbb.h" | ||
15 | #include <netinet/tcp.h> | ||
16 | |||
17 | enum { | ||
18 | FTP_DATACONN = 150, | ||
19 | FTP_NOOPOK = 200, | ||
20 | FTP_TYPEOK = 200, | ||
21 | FTP_PORTOK = 200, | ||
22 | FTP_STRUOK = 200, | ||
23 | FTP_MODEOK = 200, | ||
24 | FTP_ALLOOK = 202, | ||
25 | FTP_STATOK = 211, | ||
26 | FTP_STATFILE_OK = 213, | ||
27 | FTP_HELP = 214, | ||
28 | FTP_SYSTOK = 215, | ||
29 | FTP_GREET = 220, | ||
30 | FTP_GOODBYE = 221, | ||
31 | FTP_TRANSFEROK = 226, | ||
32 | FTP_PASVOK = 227, | ||
33 | FTP_LOGINOK = 230, | ||
34 | FTP_CWDOK = 250, | ||
35 | #if ENABLE_FEATURE_FTP_WRITE | ||
36 | FTP_RMDIROK = 250, | ||
37 | FTP_DELEOK = 250, | ||
38 | FTP_RENAMEOK = 250, | ||
39 | #endif | ||
40 | FTP_PWDOK = 257, | ||
41 | #if ENABLE_FEATURE_FTP_WRITE | ||
42 | FTP_MKDIROK = 257, | ||
43 | #endif | ||
44 | FTP_GIVEPWORD = 331, | ||
45 | FTP_RESTOK = 350, | ||
46 | #if ENABLE_FEATURE_FTP_WRITE | ||
47 | FTP_RNFROK = 350, | ||
48 | #endif | ||
49 | FTP_BADSENDCONN = 425, | ||
50 | FTP_BADSENDNET = 426, | ||
51 | FTP_BADSENDFILE = 451, | ||
52 | FTP_BADCMD = 500, | ||
53 | FTP_COMMANDNOTIMPL = 502, | ||
54 | FTP_NEEDUSER = 503, | ||
55 | FTP_NEEDRNFR = 503, | ||
56 | FTP_BADSTRU = 504, | ||
57 | FTP_BADMODE = 504, | ||
58 | FTP_LOGINERR = 530, | ||
59 | FTP_FILEFAIL = 550, | ||
60 | FTP_NOPERM = 550, | ||
61 | FTP_UPLOADFAIL = 553, | ||
62 | |||
63 | #define mk_const4(a,b,c,d) (((a * 0x100 + b) * 0x100 + c) * 0x100 + d) | ||
64 | #define mk_const3(a,b,c) ((a * 0x100 + b) * 0x100 + c) | ||
65 | const_ALLO = mk_const4('A', 'L', 'L', 'O'), | ||
66 | const_APPE = mk_const4('A', 'P', 'P', 'E'), | ||
67 | const_CDUP = mk_const4('C', 'D', 'U', 'P'), | ||
68 | const_CWD = mk_const3('C', 'W', 'D'), | ||
69 | const_DELE = mk_const4('D', 'E', 'L', 'E'), | ||
70 | const_HELP = mk_const4('H', 'E', 'L', 'P'), | ||
71 | const_LIST = mk_const4('L', 'I', 'S', 'T'), | ||
72 | const_MKD = mk_const3('M', 'K', 'D'), | ||
73 | const_MODE = mk_const4('M', 'O', 'D', 'E'), | ||
74 | const_NLST = mk_const4('N', 'L', 'S', 'T'), | ||
75 | const_NOOP = mk_const4('N', 'O', 'O', 'P'), | ||
76 | const_PASS = mk_const4('P', 'A', 'S', 'S'), | ||
77 | const_PASV = mk_const4('P', 'A', 'S', 'V'), | ||
78 | const_PORT = mk_const4('P', 'O', 'R', 'T'), | ||
79 | const_PWD = mk_const3('P', 'W', 'D'), | ||
80 | const_QUIT = mk_const4('Q', 'U', 'I', 'T'), | ||
81 | const_REST = mk_const4('R', 'E', 'S', 'T'), | ||
82 | const_RETR = mk_const4('R', 'E', 'T', 'R'), | ||
83 | const_RMD = mk_const3('R', 'M', 'D'), | ||
84 | const_RNFR = mk_const4('R', 'N', 'F', 'R'), | ||
85 | const_RNTO = mk_const4('R', 'N', 'T', 'O'), | ||
86 | const_STAT = mk_const4('S', 'T', 'A', 'T'), | ||
87 | const_STOR = mk_const4('S', 'T', 'O', 'R'), | ||
88 | const_STOU = mk_const4('S', 'T', 'O', 'U'), | ||
89 | const_STRU = mk_const4('S', 'T', 'R', 'U'), | ||
90 | const_SYST = mk_const4('S', 'Y', 'S', 'T'), | ||
91 | const_TYPE = mk_const4('T', 'Y', 'P', 'E'), | ||
92 | const_USER = mk_const4('U', 'S', 'E', 'R'), | ||
93 | }; | ||
94 | |||
95 | struct globals { | ||
96 | char *p_control_line_buf; | ||
97 | len_and_sockaddr *local_addr; | ||
98 | len_and_sockaddr *port_addr; | ||
99 | int pasv_listen_fd; | ||
100 | int data_fd; | ||
101 | off_t restart_pos; | ||
102 | char *ftp_cmp; | ||
103 | char *ftp_arg; | ||
104 | #if ENABLE_FEATURE_FTP_WRITE | ||
105 | char *rnfr_filename; | ||
106 | smallint write_enable; | ||
107 | #endif | ||
108 | }; | ||
109 | #define G (*(struct globals*)&bb_common_bufsiz1) | ||
110 | #define INIT_G() do { } while (0) | ||
111 | |||
112 | |||
113 | static char * | ||
114 | replace_text(const char *str, const char from, const char *to) | ||
115 | { | ||
116 | size_t retlen, remainlen, chunklen, tolen; | ||
117 | const char *remain; | ||
118 | char *ret, *found; | ||
119 | |||
120 | remain = str; | ||
121 | remainlen = strlen(str); | ||
122 | |||
123 | tolen = strlen(to); | ||
124 | |||
125 | /* simply alloc strlen(str)*strlen(to). To is max 2 so it's allowed */ | ||
126 | ret = xmalloc(remainlen * strlen(to) + 1); | ||
127 | retlen = 0; | ||
128 | |||
129 | for (;;) { | ||
130 | found = strchr(remain, from); | ||
131 | if (found != NULL) { | ||
132 | chunklen = found - remain; | ||
133 | |||
134 | /* Copy chunk which doesn't contain 'from' to ret */ | ||
135 | memcpy(&ret[retlen], remain, chunklen); | ||
136 | retlen += chunklen; | ||
137 | |||
138 | /* Now copy 'to' instead of 'from' */ | ||
139 | memcpy(&ret[retlen], to, tolen); | ||
140 | retlen += tolen; | ||
141 | |||
142 | remain = found + 1; | ||
143 | } else { | ||
144 | /* | ||
145 | * The last chunk. We are already sure that we have enough space | ||
146 | * so we can use strcpy. | ||
147 | */ | ||
148 | strcpy(&ret[retlen], remain); | ||
149 | break; | ||
150 | } | ||
151 | } | ||
152 | return ret; | ||
153 | } | ||
154 | |||
155 | static void | ||
156 | replace_char(char *str, char from, char to) | ||
157 | { | ||
158 | char *ptr; | ||
159 | |||
160 | /* Don't use strchr here...*/ | ||
161 | while ((ptr = strchr(str, from)) != NULL) { | ||
162 | *ptr = to; | ||
163 | str = ptr + 1; | ||
164 | } | ||
165 | } | ||
166 | |||
167 | static void | ||
168 | str_netfd_write(const char *str, int fd) | ||
169 | { | ||
170 | xwrite(fd, str, strlen(str)); | ||
171 | } | ||
172 | |||
173 | static void | ||
174 | ftp_write_str_common(unsigned int status, const char *str, char sep) | ||
175 | { | ||
176 | char *escaped_str, *response; | ||
177 | size_t len; | ||
178 | |||
179 | escaped_str = replace_text(str, '\377', "\377\377"); | ||
180 | |||
181 | response = xasprintf("%u%c%s\r\n", status, sep, escaped_str); | ||
182 | free(escaped_str); | ||
183 | |||
184 | len = strlen(response); | ||
185 | replace_char(escaped_str, '\n', '\0'); | ||
186 | |||
187 | /* Change trailing '\0' back to '\n' */ | ||
188 | response[len - 1] = '\n'; | ||
189 | xwrite(STDIN_FILENO, response, len); | ||
190 | } | ||
191 | |||
192 | static void | ||
193 | cmdio_write(int status, const char *p_text) | ||
194 | { | ||
195 | ftp_write_str_common(status, p_text, ' '); | ||
196 | } | ||
197 | |||
198 | static void | ||
199 | cmdio_write_hyphen(int status, const char *p_text) | ||
200 | { | ||
201 | ftp_write_str_common(status, p_text, '-'); | ||
202 | } | ||
203 | |||
204 | static void | ||
205 | cmdio_write_raw(const char *p_text) | ||
206 | { | ||
207 | str_netfd_write(p_text, STDIN_FILENO); | ||
208 | } | ||
209 | |||
210 | static uint32_t | ||
211 | cmdio_get_cmd_and_arg(void) | ||
212 | { | ||
213 | int len; | ||
214 | uint32_t cmdval; | ||
215 | char *cmd; | ||
216 | |||
217 | free(G.ftp_cmp); | ||
218 | G.ftp_cmp = cmd = xmalloc_reads(STDIN_FILENO, NULL, NULL); | ||
219 | /* | ||
220 | * TODO: | ||
221 | * | ||
222 | * now we should change all '\0' to '\n' - xmalloc_reads will be improved, | ||
223 | * probably | ||
224 | */ | ||
225 | len = strlen(cmd) - 1; | ||
226 | while (len >= 0 && cmd[len] == '\r') { | ||
227 | cmd[len] = '\0'; | ||
228 | len--; | ||
229 | } | ||
230 | |||
231 | G.ftp_arg = strchr(cmd, ' '); | ||
232 | if (G.ftp_arg != NULL) { | ||
233 | *G.ftp_arg = '\0'; | ||
234 | G.ftp_arg++; | ||
235 | } | ||
236 | cmdval = 0; | ||
237 | while (*cmd) | ||
238 | cmdval = (cmdval << 8) + ((unsigned char)*cmd++ & (unsigned char)~0x20); | ||
239 | |||
240 | return cmdval; | ||
241 | } | ||
242 | |||
243 | static void | ||
244 | init_data_sock_params(int sock_fd) | ||
245 | { | ||
246 | struct linger linger; | ||
247 | |||
248 | G.data_fd = sock_fd; | ||
249 | |||
250 | memset(&linger, 0, sizeof(linger)); | ||
251 | linger.l_onoff = 1; | ||
252 | linger.l_linger = 32767; | ||
253 | |||
254 | setsockopt(sock_fd, SOL_SOCKET, SO_KEEPALIVE, &const_int_1, sizeof(const_int_1)); | ||
255 | setsockopt(sock_fd, SOL_SOCKET, SO_LINGER, &linger, sizeof(linger)); | ||
256 | } | ||
257 | |||
258 | static int | ||
259 | ftpdataio_get_pasv_fd(void) | ||
260 | { | ||
261 | int remote_fd; | ||
262 | |||
263 | remote_fd = accept(G.pasv_listen_fd, NULL, 0); | ||
264 | |||
265 | if (remote_fd < 0) { | ||
266 | cmdio_write(FTP_BADSENDCONN, "Can't establish connection"); | ||
267 | return remote_fd; | ||
268 | } | ||
269 | |||
270 | init_data_sock_params(remote_fd); | ||
271 | return remote_fd; | ||
272 | } | ||
273 | |||
274 | static int | ||
275 | ftpdataio_get_port_fd(void) | ||
276 | { | ||
277 | int remote_fd; | ||
278 | |||
279 | /* Do we want die or print error to client? */ | ||
280 | remote_fd = xconnect_stream(G.port_addr); | ||
281 | |||
282 | init_data_sock_params(remote_fd); | ||
283 | return remote_fd; | ||
284 | } | ||
285 | |||
286 | static void | ||
287 | ftpdataio_dispose_transfer_fd(void) | ||
288 | { | ||
289 | /* This close() blocks because we set SO_LINGER */ | ||
290 | if (G.data_fd > STDOUT_FILENO) { | ||
291 | if (close(G.data_fd) < 0) { | ||
292 | /* Do it again without blocking. */ | ||
293 | struct linger linger; | ||
294 | |||
295 | memset(&linger, 0, sizeof(linger)); | ||
296 | setsockopt(G.data_fd, SOL_SOCKET, SO_LINGER, &linger, sizeof(linger)); | ||
297 | close(G.data_fd); | ||
298 | } | ||
299 | } | ||
300 | G.data_fd = -1; | ||
301 | } | ||
302 | |||
303 | static int | ||
304 | port_active(void) | ||
305 | { | ||
306 | return (G.port_addr != NULL) ? 1: 0; | ||
307 | } | ||
308 | |||
309 | static int | ||
310 | pasv_active(void) | ||
311 | { | ||
312 | return (G.pasv_listen_fd != -1) ? 1 : 0; | ||
313 | } | ||
314 | |||
315 | static int | ||
316 | get_remote_transfer_fd(const char *p_status_msg) | ||
317 | { | ||
318 | int remote_fd; | ||
319 | |||
320 | if (pasv_active()) | ||
321 | remote_fd = ftpdataio_get_pasv_fd(); | ||
322 | else | ||
323 | remote_fd = ftpdataio_get_port_fd(); | ||
324 | |||
325 | if (remote_fd < 0) | ||
326 | return remote_fd; | ||
327 | |||
328 | cmdio_write(FTP_DATACONN, p_status_msg); | ||
329 | return remote_fd; | ||
330 | } | ||
331 | |||
332 | static void | ||
333 | handle_pwd(void) | ||
334 | { | ||
335 | char *cwd, *promoted_cwd, *response; | ||
336 | |||
337 | cwd = xrealloc_getcwd_or_warn(NULL); | ||
338 | if (cwd == NULL) | ||
339 | cwd = xstrdup(""); | ||
340 | |||
341 | /* We _have to_ promote each " to "" */ | ||
342 | promoted_cwd = replace_text(cwd, '\"', "\"\""); | ||
343 | free(cwd); | ||
344 | response = xasprintf("\"%s\"", promoted_cwd); | ||
345 | free(promoted_cwd); | ||
346 | cmdio_write(FTP_PWDOK, response); | ||
347 | free(response); | ||
348 | } | ||
349 | |||
350 | static void | ||
351 | handle_cwd(void) | ||
352 | { | ||
353 | int retval; | ||
354 | |||
355 | /* XXX Do we need check ftp_arg != NULL? */ | ||
356 | retval = chdir(G.ftp_arg); | ||
357 | if (retval == 0) | ||
358 | cmdio_write(FTP_CWDOK, "Directory changed"); | ||
359 | else | ||
360 | cmdio_write(FTP_FILEFAIL, "Can't change directory"); | ||
361 | } | ||
362 | |||
363 | static void | ||
364 | handle_cdup(void) | ||
365 | { | ||
366 | G.ftp_arg = xstrdup(".."); | ||
367 | handle_cwd(); | ||
368 | free(G.ftp_arg); | ||
369 | } | ||
370 | |||
371 | static int | ||
372 | data_transfer_checks_ok(void) | ||
373 | { | ||
374 | if (!pasv_active() && !port_active()) { | ||
375 | cmdio_write(FTP_BADSENDCONN, "Use PORT or PASV first"); | ||
376 | return 0; | ||
377 | } | ||
378 | |||
379 | return 1; | ||
380 | } | ||
381 | |||
382 | static void | ||
383 | port_cleanup(void) | ||
384 | { | ||
385 | if (G.port_addr != NULL) | ||
386 | free(G.port_addr); | ||
387 | |||
388 | G.port_addr = NULL; | ||
389 | } | ||
390 | |||
391 | static void | ||
392 | pasv_cleanup(void) | ||
393 | { | ||
394 | if (G.pasv_listen_fd > STDOUT_FILENO) | ||
395 | close(G.pasv_listen_fd); | ||
396 | G.pasv_listen_fd = -1; | ||
397 | } | ||
398 | |||
399 | static char * | ||
400 | statbuf_getperms(const struct stat *statbuf) | ||
401 | { | ||
402 | char *perms; | ||
403 | enum { r = 'r', w = 'w', x = 'x', s = 's', S = 'S' }; | ||
404 | |||
405 | perms = xmalloc(11); | ||
406 | memset(perms, '-', 10); | ||
407 | |||
408 | perms[0] = '?'; | ||
409 | switch (statbuf->st_mode & S_IFMT) { | ||
410 | case S_IFREG: perms[0] = '-'; break; | ||
411 | case S_IFDIR: perms[0] = 'd'; break; | ||
412 | case S_IFLNK: perms[0] = 'l'; break; | ||
413 | case S_IFIFO: perms[0] = 'p'; break; | ||
414 | case S_IFSOCK: perms[0] = s; break; | ||
415 | case S_IFCHR: perms[0] = 'c'; break; | ||
416 | case S_IFBLK: perms[0] = 'b'; break; | ||
417 | } | ||
418 | |||
419 | if (statbuf->st_mode & S_IRUSR) perms[1] = r; | ||
420 | if (statbuf->st_mode & S_IWUSR) perms[2] = w; | ||
421 | if (statbuf->st_mode & S_IXUSR) perms[3] = x; | ||
422 | if (statbuf->st_mode & S_IRGRP) perms[4] = r; | ||
423 | if (statbuf->st_mode & S_IWGRP) perms[5] = w; | ||
424 | if (statbuf->st_mode & S_IXGRP) perms[6] = x; | ||
425 | if (statbuf->st_mode & S_IROTH) perms[7] = r; | ||
426 | if (statbuf->st_mode & S_IWOTH) perms[8] = w; | ||
427 | if (statbuf->st_mode & S_IXOTH) perms[9] = x; | ||
428 | if (statbuf->st_mode & S_ISUID) perms[3] = (perms[3] == x) ? s : S; | ||
429 | if (statbuf->st_mode & S_ISGID) perms[6] = (perms[6] == x) ? s : S; | ||
430 | if (statbuf->st_mode & S_ISVTX) perms[9] = (perms[9] == x) ? 't' : 'T'; | ||
431 | |||
432 | perms[10] = '\0'; | ||
433 | |||
434 | return perms; | ||
435 | } | ||
436 | |||
437 | static void | ||
438 | write_filestats(int fd, const char *filename, | ||
439 | const struct stat *statbuf) | ||
440 | { | ||
441 | off_t size; | ||
442 | char *stats, *lnkname = NULL, *perms; | ||
443 | const char *name; | ||
444 | int retval; | ||
445 | char timestr[32]; | ||
446 | struct tm *tm; | ||
447 | const char *format = "%b %d %H:%M"; | ||
448 | |||
449 | name = bb_get_last_path_component_nostrip(filename); | ||
450 | |||
451 | if (statbuf != NULL) { | ||
452 | size = statbuf->st_size; | ||
453 | |||
454 | if (S_ISLNK(statbuf->st_mode)) | ||
455 | /* Damn symlink... */ | ||
456 | lnkname = xmalloc_readlink(filename); | ||
457 | |||
458 | tm = gmtime(&statbuf->st_mtime); | ||
459 | retval = strftime(timestr, sizeof(timestr), format, tm); | ||
460 | if (retval == 0) | ||
461 | bb_error_msg_and_die("strftime"); | ||
462 | |||
463 | timestr[sizeof(timestr) - 1] = '\0'; | ||
464 | |||
465 | perms = statbuf_getperms(statbuf); | ||
466 | |||
467 | stats = xasprintf("%s %u\tftp ftp %"OFF_FMT"u\t%s %s", | ||
468 | perms, (int) statbuf->st_nlink, | ||
469 | size, timestr, name); | ||
470 | |||
471 | free(perms); | ||
472 | } else | ||
473 | stats = xstrdup(name); | ||
474 | |||
475 | str_netfd_write(stats, fd); | ||
476 | free(stats); | ||
477 | if (lnkname != NULL) { | ||
478 | str_netfd_write(" -> ", fd); | ||
479 | str_netfd_write(lnkname, fd); | ||
480 | free(lnkname); | ||
481 | } | ||
482 | str_netfd_write("\r\n", fd); | ||
483 | } | ||
484 | |||
485 | static void | ||
486 | write_dirstats(int fd, const char *dname, int details) | ||
487 | { | ||
488 | DIR *dir; | ||
489 | struct dirent *dirent; | ||
490 | struct stat statbuf; | ||
491 | char *filename; | ||
492 | |||
493 | dir = xopendir(dname); | ||
494 | |||
495 | for (;;) { | ||
496 | dirent = readdir(dir); | ||
497 | if (dirent == NULL) | ||
498 | break; | ||
499 | |||
500 | /* Ignore . and .. */ | ||
501 | if (dirent->d_name[0] == '.') { | ||
502 | if (dirent->d_name[1] == '\0' | ||
503 | || (dirent->d_name[1] == '.' && dirent->d_name[2] == '\0') | ||
504 | ) { | ||
505 | continue; | ||
506 | } | ||
507 | } | ||
508 | |||
509 | if (details) { | ||
510 | filename = xasprintf("%s/%s", dname, dirent->d_name); | ||
511 | if (lstat(filename, &statbuf) != 0) { | ||
512 | free(filename); | ||
513 | goto bail; | ||
514 | } | ||
515 | } else | ||
516 | filename = xstrdup(dirent->d_name); | ||
517 | |||
518 | write_filestats(fd, filename, details ? &statbuf : NULL); | ||
519 | free(filename); | ||
520 | } | ||
521 | |||
522 | bail: | ||
523 | closedir(dir); | ||
524 | } | ||
525 | |||
526 | static void | ||
527 | handle_pasv(void) | ||
528 | { | ||
529 | int bind_retries = 10; | ||
530 | unsigned short port; | ||
531 | enum { min_port = 1024, max_port = 65535 }; | ||
532 | char *addr, *wire_addr, *response; | ||
533 | |||
534 | pasv_cleanup(); | ||
535 | port_cleanup(); | ||
536 | G.pasv_listen_fd = xsocket(G.local_addr->u.sa.sa_family, SOCK_STREAM, 0); | ||
537 | setsockopt_reuseaddr(G.pasv_listen_fd); | ||
538 | |||
539 | /* TODO bind() with port == 0 and then call getsockname */ | ||
540 | while (--bind_retries) { | ||
541 | port = rand() % max_port; | ||
542 | if (port < min_port) { | ||
543 | port += min_port; | ||
544 | } | ||
545 | |||
546 | set_nport(G.local_addr, htons(port)); | ||
547 | /* We don't want to use xbind, it'll die if port is in use */ | ||
548 | if (bind(G.pasv_listen_fd, &G.local_addr->u.sa, G.local_addr->len) != 0) { | ||
549 | /* do we want check if errno == EADDRINUSE ? */ | ||
550 | continue; | ||
551 | } | ||
552 | xlisten(G.pasv_listen_fd, 1); | ||
553 | break; | ||
554 | } | ||
555 | |||
556 | if (!bind_retries) | ||
557 | bb_error_msg_and_die("can't create pasv socket"); | ||
558 | |||
559 | addr = xmalloc_sockaddr2dotted_noport(&G.local_addr->u.sa); | ||
560 | wire_addr = replace_text(addr, '.', ","); | ||
561 | free(addr); | ||
562 | |||
563 | response = xasprintf("Entering Passive Mode (%s,%u,%u)", | ||
564 | wire_addr, (int)(port >> 8), (int)(port & 255)); | ||
565 | |||
566 | cmdio_write(FTP_PASVOK, response); | ||
567 | free(wire_addr); | ||
568 | free(response); | ||
569 | } | ||
570 | |||
571 | static void | ||
572 | handle_retr(void) | ||
573 | { | ||
574 | struct stat statbuf; | ||
575 | int trans_ret, retval; | ||
576 | int remote_fd; | ||
577 | int opened_file; | ||
578 | off_t offset = G.restart_pos; | ||
579 | char *response; | ||
580 | |||
581 | G.restart_pos = 0; | ||
582 | |||
583 | if (!data_transfer_checks_ok()) | ||
584 | return; | ||
585 | |||
586 | /* XXX Do we need check if ftp_arg != NULL? */ | ||
587 | opened_file = open(G.ftp_arg, O_RDONLY | O_NONBLOCK); | ||
588 | if (opened_file < 0) { | ||
589 | cmdio_write(FTP_FILEFAIL, "Can't open file"); | ||
590 | return; | ||
591 | } | ||
592 | |||
593 | retval = fstat(opened_file, &statbuf); | ||
594 | if (retval < 0 || !S_ISREG(statbuf.st_mode)) { | ||
595 | /* Note - pretend open failed */ | ||
596 | cmdio_write(FTP_FILEFAIL, "Can't open file"); | ||
597 | goto file_close_out; | ||
598 | } | ||
599 | |||
600 | /* Now deactive O_NONBLOCK, otherwise we have a problem on DMAPI filesystems | ||
601 | * such as XFS DMAPI. | ||
602 | */ | ||
603 | ndelay_off(opened_file); | ||
604 | |||
605 | /* Set the download offset (from REST) if any */ | ||
606 | if (offset != 0) | ||
607 | xlseek(opened_file, offset, SEEK_SET); | ||
608 | |||
609 | response = xasprintf( | ||
610 | "Opening BINARY mode data connection for (%s %"OFF_FMT"u bytes).", | ||
611 | G.ftp_arg, statbuf.st_size); | ||
612 | |||
613 | remote_fd = get_remote_transfer_fd(response); | ||
614 | free(response); | ||
615 | if (remote_fd < 0) | ||
616 | goto port_pasv_cleanup_out; | ||
617 | |||
618 | trans_ret = bb_copyfd_eof(opened_file, remote_fd); | ||
619 | ftpdataio_dispose_transfer_fd(); | ||
620 | if (trans_ret < 0) | ||
621 | cmdio_write(FTP_BADSENDFILE, "Error sending local file"); | ||
622 | else | ||
623 | cmdio_write(FTP_TRANSFEROK, "File sent OK"); | ||
624 | |||
625 | port_pasv_cleanup_out: | ||
626 | port_cleanup(); | ||
627 | pasv_cleanup(); | ||
628 | file_close_out: | ||
629 | close(opened_file); | ||
630 | } | ||
631 | |||
632 | static void | ||
633 | handle_dir_common(int full_details, int stat_cmd) | ||
634 | { | ||
635 | int fd; | ||
636 | struct stat statbuf; | ||
637 | |||
638 | if (!stat_cmd && !data_transfer_checks_ok()) | ||
639 | return; | ||
640 | |||
641 | if (stat_cmd) { | ||
642 | fd = STDIN_FILENO; | ||
643 | cmdio_write_hyphen(FTP_STATFILE_OK, "Status follows:"); | ||
644 | } else { | ||
645 | fd = get_remote_transfer_fd("Here comes the directory listing"); | ||
646 | if (fd < 0) | ||
647 | goto bail; | ||
648 | } | ||
649 | |||
650 | if (G.ftp_arg != NULL) { | ||
651 | if (lstat(G.ftp_arg, &statbuf) != 0) { | ||
652 | /* Dir doesn't exist => return ok to client */ | ||
653 | goto bail; | ||
654 | } | ||
655 | if (S_ISREG(statbuf.st_mode) || S_ISLNK(statbuf.st_mode)) | ||
656 | write_filestats(fd, G.ftp_arg, &statbuf); | ||
657 | else if (S_ISDIR(statbuf.st_mode)) | ||
658 | write_dirstats(fd, G.ftp_arg, full_details); | ||
659 | } else | ||
660 | write_dirstats(fd, ".", full_details); | ||
661 | |||
662 | bail: | ||
663 | /* Well, if we can't open directory/file it doesn't matter */ | ||
664 | if (!stat_cmd) { | ||
665 | ftpdataio_dispose_transfer_fd(); | ||
666 | pasv_cleanup(); | ||
667 | port_cleanup(); | ||
668 | cmdio_write(FTP_TRANSFEROK, "OK"); | ||
669 | } else | ||
670 | cmdio_write(FTP_STATFILE_OK, "End of status"); | ||
671 | } | ||
672 | |||
673 | static void | ||
674 | handle_list(void) | ||
675 | { | ||
676 | handle_dir_common(1, 0); | ||
677 | } | ||
678 | |||
679 | static void | ||
680 | handle_type(void) | ||
681 | { | ||
682 | if (G.ftp_arg | ||
683 | && ( ((G.ftp_arg[0] | 0x20) == 'i' && G.ftp_arg[1] == '\0') | ||
684 | || !strcasecmp(G.ftp_arg, "L8") | ||
685 | || !strcasecmp(G.ftp_arg, "L 8") | ||
686 | ) | ||
687 | ) { | ||
688 | cmdio_write(FTP_TYPEOK, "Switching to Binary mode"); | ||
689 | } else { | ||
690 | cmdio_write(FTP_BADCMD, "Unrecognised TYPE command"); | ||
691 | } | ||
692 | } | ||
693 | |||
694 | static void | ||
695 | handle_port(void) | ||
696 | { | ||
697 | unsigned short port; | ||
698 | char *raw = NULL, *port_part; | ||
699 | len_and_sockaddr *lsa = NULL; | ||
700 | |||
701 | pasv_cleanup(); | ||
702 | port_cleanup(); | ||
703 | |||
704 | if (G.ftp_arg == NULL) | ||
705 | goto bail; | ||
706 | |||
707 | raw = replace_text(G.ftp_arg, ',', "."); | ||
708 | |||
709 | port_part = strrchr(raw, '.'); | ||
710 | if (port_part == NULL) | ||
711 | goto bail; | ||
712 | |||
713 | port = xatou16(&port_part[1]); | ||
714 | *port_part = '\0'; | ||
715 | |||
716 | port_part = strrchr(raw, '.'); | ||
717 | if (port_part == NULL) | ||
718 | goto bail; | ||
719 | |||
720 | port |= xatou16(&port_part[1]) << 8; | ||
721 | *port_part = '\0'; | ||
722 | |||
723 | lsa = xdotted2sockaddr(raw, port); | ||
724 | |||
725 | bail: | ||
726 | free(raw); | ||
727 | |||
728 | if (lsa == NULL) { | ||
729 | cmdio_write(FTP_BADCMD, "Illegal PORT command"); | ||
730 | return; | ||
731 | } | ||
732 | |||
733 | G.port_addr = lsa; | ||
734 | cmdio_write(FTP_PORTOK, "PORT command successful. Consider using PASV"); | ||
735 | } | ||
736 | |||
737 | #if ENABLE_FEATURE_FTP_WRITE | ||
738 | static void | ||
739 | handle_upload_common(int is_append, int is_unique) | ||
740 | { | ||
741 | char *template = NULL; | ||
742 | int trans_ret; | ||
743 | int new_file_fd; | ||
744 | int remote_fd; | ||
745 | |||
746 | enum { | ||
747 | fileflags = O_CREAT | O_WRONLY | O_APPEND, | ||
748 | }; | ||
749 | |||
750 | off_t offset = G.restart_pos; | ||
751 | |||
752 | G.restart_pos = 0; | ||
753 | if (!data_transfer_checks_ok()) | ||
754 | return; | ||
755 | |||
756 | if (is_unique) { | ||
757 | template = xstrdup("uniq.XXXXXX"); | ||
758 | /* | ||
759 | * XXX Use mkostemp here? vsftpd opens file with O_CREAT, O_WRONLY, | ||
760 | * O_APPEND and O_EXCL flags... | ||
761 | */ | ||
762 | new_file_fd = mkstemp(template); | ||
763 | } else { | ||
764 | /* XXX Do we need check if ftp_arg != NULL? */ | ||
765 | if (!is_append && offset == 0) | ||
766 | new_file_fd = open(G.ftp_arg, O_CREAT | O_WRONLY | O_APPEND | O_NONBLOCK | O_TRUNC, 0666); | ||
767 | else | ||
768 | new_file_fd = open(G.ftp_arg, O_CREAT | O_WRONLY | O_APPEND | O_NONBLOCK, 0666); | ||
769 | } | ||
770 | |||
771 | if (new_file_fd < 0) { | ||
772 | cmdio_write(FTP_UPLOADFAIL, "Can't create file"); | ||
773 | return; | ||
774 | } | ||
775 | |||
776 | if (!is_append && offset != 0) { | ||
777 | /* warning, allows seek past end of file! Check for seek > size? */ | ||
778 | xlseek(new_file_fd, offset, SEEK_SET); | ||
779 | } | ||
780 | |||
781 | if (is_unique) { | ||
782 | char *resp = xasprintf("FILE: %s", template); | ||
783 | remote_fd = get_remote_transfer_fd(resp); | ||
784 | free(resp); | ||
785 | free(template); | ||
786 | } else | ||
787 | remote_fd = get_remote_transfer_fd("Ok to send data"); | ||
788 | |||
789 | if (remote_fd < 0) | ||
790 | goto bail; | ||
791 | |||
792 | trans_ret = bb_copyfd_eof(remote_fd, new_file_fd); | ||
793 | ftpdataio_dispose_transfer_fd(); | ||
794 | |||
795 | if (trans_ret < 0) | ||
796 | cmdio_write(FTP_BADSENDFILE, "Failure writing to local file"); | ||
797 | else | ||
798 | cmdio_write(FTP_TRANSFEROK, "File receive OK"); | ||
799 | |||
800 | bail: | ||
801 | port_cleanup(); | ||
802 | pasv_cleanup(); | ||
803 | close(new_file_fd); | ||
804 | } | ||
805 | |||
806 | static void | ||
807 | handle_stor(void) | ||
808 | { | ||
809 | handle_upload_common(0, 0); | ||
810 | } | ||
811 | |||
812 | static void | ||
813 | handle_mkd(void) | ||
814 | { | ||
815 | int retval; | ||
816 | |||
817 | /* Do we need check if ftp_arg != NULL? */ | ||
818 | retval = mkdir(G.ftp_arg, 0770); | ||
819 | if (retval != 0) { | ||
820 | cmdio_write(FTP_FILEFAIL, "Create directory operation failed"); | ||
821 | return; | ||
822 | } | ||
823 | |||
824 | cmdio_write(FTP_MKDIROK, "created"); | ||
825 | } | ||
826 | |||
827 | static void | ||
828 | handle_rmd(void) | ||
829 | { | ||
830 | int retval; | ||
831 | |||
832 | /* Do we need check if ftp_arg != NULL? */ | ||
833 | retval = rmdir(G.ftp_arg); | ||
834 | if (retval != 0) | ||
835 | cmdio_write(FTP_FILEFAIL, "rmdir failed"); | ||
836 | else | ||
837 | cmdio_write(FTP_RMDIROK, "rmdir successful"); | ||
838 | } | ||
839 | |||
840 | static void | ||
841 | handle_dele(void) | ||
842 | { | ||
843 | int retval; | ||
844 | |||
845 | /* Do we need check if ftp_arg != NULL? */ | ||
846 | retval = unlink(G.ftp_arg); | ||
847 | if (retval != 0) | ||
848 | cmdio_write(FTP_FILEFAIL, "Delete failed"); | ||
849 | else | ||
850 | cmdio_write(FTP_DELEOK, "Delete successful"); | ||
851 | } | ||
852 | #endif /* ENABLE_FEATURE_FTP_WRITE */ | ||
853 | |||
854 | static void | ||
855 | handle_rest(void) | ||
856 | { | ||
857 | /* When ftp_arg == NULL simply restart from beginning */ | ||
858 | G.restart_pos = xatoi_u(G.ftp_arg); | ||
859 | cmdio_write(FTP_RESTOK, "Restart OK"); | ||
860 | } | ||
861 | |||
862 | #if ENABLE_FEATURE_FTP_WRITE | ||
863 | static void | ||
864 | handle_rnfr(void) | ||
865 | { | ||
866 | struct stat statbuf; | ||
867 | int retval; | ||
868 | |||
869 | /* Clear old value */ | ||
870 | free(G.rnfr_filename); | ||
871 | |||
872 | /* Does it exist? Do we need check if ftp_arg != NULL? */ | ||
873 | retval = stat(G.ftp_arg, &statbuf); | ||
874 | if (retval == 0) { | ||
875 | /* Yes */ | ||
876 | G.rnfr_filename = xstrdup(G.ftp_arg); | ||
877 | cmdio_write(FTP_RNFROK, "Ready for RNTO"); | ||
878 | } else | ||
879 | cmdio_write(FTP_FILEFAIL, "RNFR command failed"); | ||
880 | } | ||
881 | |||
882 | static void | ||
883 | handle_rnto(void) | ||
884 | { | ||
885 | int retval; | ||
886 | |||
887 | /* If we didn't get a RNFR, throw a wobbly */ | ||
888 | if (G.rnfr_filename == NULL) { | ||
889 | cmdio_write(FTP_NEEDRNFR, "RNFR required first"); | ||
890 | return; | ||
891 | } | ||
892 | |||
893 | /* XXX Do we need check if ftp_arg != NULL? */ | ||
894 | retval = rename(G.rnfr_filename, G.ftp_arg); | ||
895 | |||
896 | free(G.rnfr_filename); | ||
897 | |||
898 | if (retval == 0) | ||
899 | cmdio_write(FTP_RENAMEOK, "Rename successful"); | ||
900 | else | ||
901 | cmdio_write(FTP_FILEFAIL, "Rename failed"); | ||
902 | } | ||
903 | #endif /* ENABLE_FEATURE_FTP_WRITE */ | ||
904 | |||
905 | static void | ||
906 | handle_nlst(void) | ||
907 | { | ||
908 | handle_dir_common(0, 0); | ||
909 | } | ||
910 | |||
911 | #if ENABLE_FEATURE_FTP_WRITE | ||
912 | static void | ||
913 | handle_appe(void) | ||
914 | { | ||
915 | handle_upload_common(1, 0); | ||
916 | } | ||
917 | #endif | ||
918 | |||
919 | static void | ||
920 | handle_help(void) | ||
921 | { | ||
922 | cmdio_write_hyphen(FTP_HELP, "Recognized commands:"); | ||
923 | cmdio_write_raw(" ALLO CDUP CWD HELP LIST\r\n" | ||
924 | " MODE NLST NOOP PASS PASV PORT PWD QUIT\r\n" | ||
925 | " REST RETR STAT STRU SYST TYPE USER\r\n" | ||
926 | #if ENABLE_FEATURE_FTP_WRITE | ||
927 | " APPE DELE MKD RMD RNFR RNTO STOR STOU\r\n" | ||
928 | #endif | ||
929 | ); | ||
930 | cmdio_write(FTP_HELP, "Help OK"); | ||
931 | } | ||
932 | |||
933 | #if ENABLE_FEATURE_FTP_WRITE | ||
934 | static void | ||
935 | handle_stou(void) | ||
936 | { | ||
937 | handle_upload_common(0, 1); | ||
938 | } | ||
939 | #endif | ||
940 | |||
941 | static void | ||
942 | handle_stat(void) | ||
943 | { | ||
944 | cmdio_write_hyphen(FTP_STATOK, "FTP server status:"); | ||
945 | cmdio_write_raw(" TYPE: BINARY\r\n"); | ||
946 | cmdio_write(FTP_STATOK, "End of status"); | ||
947 | } | ||
948 | |||
949 | static void | ||
950 | handle_stat_file(void) | ||
951 | { | ||
952 | handle_dir_common(1, 1); | ||
953 | } | ||
954 | |||
955 | /* TODO: libbb candidate (tftp has another copy) */ | ||
956 | static len_and_sockaddr *get_sock_lsa(int s) | ||
957 | { | ||
958 | len_and_sockaddr *lsa; | ||
959 | socklen_t len = 0; | ||
960 | |||
961 | if (getsockname(s, NULL, &len) != 0) | ||
962 | return NULL; | ||
963 | lsa = xzalloc(LSA_LEN_SIZE + len); | ||
964 | lsa->len = len; | ||
965 | getsockname(s, &lsa->u.sa, &lsa->len); | ||
966 | return lsa; | ||
967 | } | ||
968 | |||
969 | int ftpd_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; | ||
970 | int ftpd_main(int argc UNUSED_PARAM, char **argv) | ||
971 | { | ||
972 | smallint user_was_specified = 0; | ||
973 | |||
974 | INIT_G(); | ||
975 | |||
976 | G.local_addr = get_sock_lsa(STDIN_FILENO); | ||
977 | if (!G.local_addr) { | ||
978 | /* This is confusing: | ||
979 | * bb_error_msg_and_die("stdin is not a socket"); | ||
980 | * Better: */ | ||
981 | bb_show_usage(); | ||
982 | /* Help text says that ftpd must be used as inetd service, | ||
983 | * which is by far the most usual cause of get_sock_lsa | ||
984 | * failure */ | ||
985 | } | ||
986 | |||
987 | logmode = LOGMODE_SYSLOG; | ||
988 | |||
989 | USE_FEATURE_FTP_WRITE(G.write_enable =) getopt32(argv, "" USE_FEATURE_FTP_WRITE("w")); | ||
990 | if (argv[optind]) { | ||
991 | xchdir(argv[optind]); | ||
992 | chroot("."); | ||
993 | } | ||
994 | |||
995 | // if (G.local_addr->u.sa.sa_family != AF_INET) | ||
996 | // bb_error_msg_and_die("Only IPv4 is supported"); | ||
997 | |||
998 | /* Signals. We'll always take -EPIPE rather than a rude signal, thanks */ | ||
999 | signal(SIGPIPE, SIG_IGN); | ||
1000 | |||
1001 | /* Set up options on the command socket */ | ||
1002 | setsockopt(STDIN_FILENO, IPPROTO_TCP, TCP_NODELAY, &const_int_1, sizeof(const_int_1)); | ||
1003 | setsockopt(STDIN_FILENO, SOL_SOCKET, SO_KEEPALIVE, &const_int_1, sizeof(const_int_1)); | ||
1004 | setsockopt(STDIN_FILENO, SOL_SOCKET, SO_OOBINLINE, &const_int_1, sizeof(const_int_1)); | ||
1005 | |||
1006 | cmdio_write(FTP_GREET, "Welcome"); | ||
1007 | |||
1008 | while (1) { | ||
1009 | uint32_t cmdval = cmdio_get_cmd_and_arg(); | ||
1010 | |||
1011 | if (cmdval == const_USER) { | ||
1012 | if (G.ftp_arg == NULL || strcasecmp(G.ftp_arg, "anonymous") != 0) | ||
1013 | cmdio_write(FTP_LOGINERR, "Server is anonymous only"); | ||
1014 | else { | ||
1015 | user_was_specified = 1; | ||
1016 | cmdio_write(FTP_GIVEPWORD, "Please specify the password"); | ||
1017 | } | ||
1018 | } else if (cmdval == const_PASS) { | ||
1019 | if (user_was_specified) | ||
1020 | break; | ||
1021 | cmdio_write(FTP_NEEDUSER, "Login with USER"); | ||
1022 | } else if (cmdval == const_QUIT) { | ||
1023 | cmdio_write(FTP_GOODBYE, "Goodbye"); | ||
1024 | return 0; | ||
1025 | } else { | ||
1026 | cmdio_write(FTP_LOGINERR, | ||
1027 | "Login with USER and PASS"); | ||
1028 | } | ||
1029 | } | ||
1030 | |||
1031 | umask(077); | ||
1032 | cmdio_write(FTP_LOGINOK, "Login successful"); | ||
1033 | |||
1034 | while (1) { | ||
1035 | uint32_t cmdval = cmdio_get_cmd_and_arg(); | ||
1036 | |||
1037 | if (cmdval == const_QUIT) { | ||
1038 | cmdio_write(FTP_GOODBYE, "Goodbye"); | ||
1039 | return 0; | ||
1040 | } | ||
1041 | if (cmdval == const_PWD) | ||
1042 | handle_pwd(); | ||
1043 | else if (cmdval == const_CWD) | ||
1044 | handle_cwd(); | ||
1045 | else if (cmdval == const_CDUP) | ||
1046 | handle_cdup(); | ||
1047 | else if (cmdval == const_PASV) | ||
1048 | handle_pasv(); | ||
1049 | else if (cmdval == const_RETR) | ||
1050 | handle_retr(); | ||
1051 | else if (cmdval == const_NOOP) | ||
1052 | cmdio_write(FTP_NOOPOK, "NOOP ok"); | ||
1053 | else if (cmdval == const_SYST) | ||
1054 | cmdio_write(FTP_SYSTOK, "UNIX Type: L8"); | ||
1055 | else if (cmdval == const_HELP) | ||
1056 | handle_help(); | ||
1057 | else if (cmdval == const_LIST) | ||
1058 | handle_list(); | ||
1059 | else if (cmdval == const_TYPE) | ||
1060 | handle_type(); | ||
1061 | else if (cmdval == const_PORT) | ||
1062 | handle_port(); | ||
1063 | else if (cmdval == const_REST) | ||
1064 | handle_rest(); | ||
1065 | else if (cmdval == const_NLST) | ||
1066 | handle_nlst(); | ||
1067 | #if ENABLE_FEATURE_FTP_WRITE | ||
1068 | else if (G.write_enable) { | ||
1069 | if (cmdval == const_STOR) | ||
1070 | handle_stor(); | ||
1071 | else if (cmdval == const_MKD) | ||
1072 | handle_mkd(); | ||
1073 | else if (cmdval == const_RMD) | ||
1074 | handle_rmd(); | ||
1075 | else if (cmdval == const_DELE) | ||
1076 | handle_dele(); | ||
1077 | else if (cmdval == const_RNFR) | ||
1078 | handle_rnfr(); | ||
1079 | else if (cmdval == const_RNTO) | ||
1080 | handle_rnto(); | ||
1081 | else if (cmdval == const_APPE) | ||
1082 | handle_appe(); | ||
1083 | else if (cmdval == const_STOU) | ||
1084 | handle_stou(); | ||
1085 | } | ||
1086 | #endif | ||
1087 | else if (cmdval == const_STRU) { | ||
1088 | if (G.ftp_arg | ||
1089 | && (G.ftp_arg[0] | 0x20) == 'f' | ||
1090 | && G.ftp_arg[1] == '\0' | ||
1091 | ) { | ||
1092 | cmdio_write(FTP_STRUOK, "Structure set to F"); | ||
1093 | } else | ||
1094 | cmdio_write(FTP_BADSTRU, "Bad STRU command"); | ||
1095 | |||
1096 | } else if (cmdval == const_MODE) { | ||
1097 | if (G.ftp_arg | ||
1098 | && (G.ftp_arg[0] | 0x20) == 's' | ||
1099 | && G.ftp_arg[1] == '\0' | ||
1100 | ) { | ||
1101 | cmdio_write(FTP_MODEOK, "Mode set to S"); | ||
1102 | } else | ||
1103 | cmdio_write(FTP_BADMODE, "Bad MODE command"); | ||
1104 | } | ||
1105 | else if (cmdval == const_ALLO) | ||
1106 | cmdio_write(FTP_ALLOOK, "ALLO command ignored"); | ||
1107 | else if (cmdval == const_STAT) { | ||
1108 | if (G.ftp_arg == NULL) | ||
1109 | handle_stat(); | ||
1110 | else | ||
1111 | handle_stat_file(); | ||
1112 | } else if (cmdval == const_USER) | ||
1113 | cmdio_write(FTP_LOGINERR, "Can't change to another user"); | ||
1114 | else if (cmdval == const_PASS) | ||
1115 | cmdio_write(FTP_LOGINOK, "Already logged in"); | ||
1116 | else if (cmdval == const_STOR | ||
1117 | || cmdval == const_MKD | ||
1118 | || cmdval == const_RMD | ||
1119 | || cmdval == const_DELE | ||
1120 | || cmdval == const_RNFR | ||
1121 | || cmdval == const_RNTO | ||
1122 | || cmdval == const_APPE | ||
1123 | || cmdval == const_STOU | ||
1124 | ) { | ||
1125 | cmdio_write(FTP_NOPERM, "Permission denied"); | ||
1126 | } else { | ||
1127 | cmdio_write(FTP_BADCMD, "Unknown command"); | ||
1128 | } | ||
1129 | } | ||
1130 | } | ||