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 * Licensed under GPLv2, see file LICENSE in this tarball for details.
8 *
9 * Only subset of FTP protocol is implemented but vast majority of clients
10 * should not have any problem.
11 *
12 * You have to run this daemon via inetd.
13 */
14
15#include "libbb.h"
16#include <syslog.h>
17#include <netinet/tcp.h>
18
19#define FTP_DATACONN            150
20#define FTP_NOOPOK              200
21#define FTP_TYPEOK              200
22#define FTP_PORTOK              200
23#define FTP_STRUOK              200
24#define FTP_MODEOK              200
25#define FTP_ALLOOK              202
26#define FTP_STATOK              211
27#define FTP_STATFILE_OK         213
28#define FTP_HELP                214
29#define FTP_SYSTOK              215
30#define FTP_GREET               220
31#define FTP_GOODBYE             221
32#define FTP_TRANSFEROK          226
33#define FTP_PASVOK              227
34/*#define FTP_EPRTOK              228*/
35#define FTP_EPSVOK              229
36#define FTP_LOGINOK             230
37#define FTP_CWDOK               250
38#define FTP_RMDIROK             250
39#define FTP_DELEOK              250
40#define FTP_RENAMEOK            250
41#define FTP_PWDOK               257
42#define FTP_MKDIROK             257
43#define FTP_GIVEPWORD           331
44#define FTP_RESTOK              350
45#define FTP_RNFROK              350
46#define FTP_TIMEOUT             421
47#define FTP_BADSENDCONN         425
48#define FTP_BADSENDNET          426
49#define FTP_BADSENDFILE         451
50#define FTP_BADCMD              500
51#define FTP_COMMANDNOTIMPL      502
52#define FTP_NEEDUSER            503
53#define FTP_NEEDRNFR            503
54#define FTP_BADSTRU             504
55#define FTP_BADMODE             504
56#define FTP_LOGINERR            530
57#define FTP_FILEFAIL            550
58#define FTP_NOPERM              550
59#define FTP_UPLOADFAIL          553
60
61#define STR1(s) #s
62#define STR(s) STR1(s)
63
64/* Convert a constant to 3-digit string, packed into uint32_t */
65enum {
66	/* Shift for Nth decimal digit */
67	SHIFT2  =  0 * BB_LITTLE_ENDIAN + 24 * BB_BIG_ENDIAN,
68	SHIFT1  =  8 * BB_LITTLE_ENDIAN + 16 * BB_BIG_ENDIAN,
69	SHIFT0  = 16 * BB_LITTLE_ENDIAN + 8 * BB_BIG_ENDIAN,
70	/* And for 4th position (space) */
71	SHIFTsp = 24 * BB_LITTLE_ENDIAN + 0 * BB_BIG_ENDIAN,
72};
73#define STRNUM32(s) (uint32_t)(0 \
74	| (('0' + ((s) / 1 % 10)) << SHIFT0) \
75	| (('0' + ((s) / 10 % 10)) << SHIFT1) \
76	| (('0' + ((s) / 100 % 10)) << SHIFT2) \
77)
78#define STRNUM32sp(s) (uint32_t)(0 \
79	| (' ' << SHIFTsp) \
80	| (('0' + ((s) / 1 % 10)) << SHIFT0) \
81	| (('0' + ((s) / 10 % 10)) << SHIFT1) \
82	| (('0' + ((s) / 100 % 10)) << SHIFT2) \
83)
84
85#define MSG_OK "Operation successful\r\n"
86#define MSG_ERR "Error\r\n"
87
88struct globals {
89	int pasv_listen_fd;
90#if !BB_MMU
91	int root_fd;
92#endif
93	int local_file_fd;
94	unsigned end_time;
95	unsigned timeout;
96	unsigned verbose;
97	off_t local_file_pos;
98	off_t restart_pos;
99	len_and_sockaddr *local_addr;
100	len_and_sockaddr *port_addr;
101	char *ftp_cmd;
102	char *ftp_arg;
103#if ENABLE_FEATURE_FTP_WRITE
104	char *rnfr_filename;
105#endif
106	/* We need these aligned to uint32_t */
107	char msg_ok [(sizeof("NNN " MSG_OK ) + 3) & 0xfffc];
108	char msg_err[(sizeof("NNN " MSG_ERR) + 3) & 0xfffc];
109} FIX_ALIASING;
110#define G (*(struct globals*)&bb_common_bufsiz1)
111#define INIT_G() do { \
112	/* Moved to main */ \
113	/*strcpy(G.msg_ok  + 4, MSG_OK );*/ \
114	/*strcpy(G.msg_err + 4, MSG_ERR);*/ \
115} while (0)
116
117
118static char *
119escape_text(const char *prepend, const char *str, unsigned escapee)
120{
121	unsigned retlen, remainlen, chunklen;
122	char *ret, *found;
123	char append;
124
125	append = (char)escapee;
126	escapee >>= 8;
127
128	remainlen = strlen(str);
129	retlen = strlen(prepend);
130	ret = xmalloc(retlen + remainlen * 2 + 1 + 1);
131	strcpy(ret, prepend);
132
133	for (;;) {
134		found = strchrnul(str, escapee);
135		chunklen = found - str + 1;
136
137		/* Copy chunk up to and including escapee (or NUL) to ret */
138		memcpy(ret + retlen, str, chunklen);
139		retlen += chunklen;
140
141		if (*found == '\0') {
142			/* It wasn't escapee, it was NUL! */
143			ret[retlen - 1] = append; /* replace NUL */
144			ret[retlen] = '\0'; /* add NUL */
145			break;
146		}
147		ret[retlen++] = escapee; /* duplicate escapee */
148		str = found + 1;
149	}
150	return ret;
151}
152
153/* Returns strlen as a bonus */
154static unsigned
155replace_char(char *str, char from, char to)
156{
157	char *p = str;
158	while (*p) {
159		if (*p == from)
160			*p = to;
161		p++;
162	}
163	return p - str;
164}
165
166static void
167verbose_log(const char *str)
168{
169	bb_error_msg("%.*s", (int)strcspn(str, "\r\n"), str);
170}
171
172/* NB: status_str is char[4] packed into uint32_t */
173static void
174cmdio_write(uint32_t status_str, const char *str)
175{
176	char *response;
177	int len;
178
179	/* FTP uses telnet protocol for command link.
180	 * In telnet, 0xff is an escape char, and needs to be escaped: */
181	response = escape_text((char *) &status_str, str, (0xff << 8) + '\r');
182
183	/* FTP sends embedded LFs as NULs */
184	len = replace_char(response, '\n', '\0');
185
186	response[len++] = '\n'; /* tack on trailing '\n' */
187	xwrite(STDOUT_FILENO, response, len);
188	if (G.verbose > 1)
189		verbose_log(response);
190	free(response);
191}
192
193static void
194cmdio_write_ok(unsigned status)
195{
196	*(uint32_t *) G.msg_ok = status;
197	xwrite(STDOUT_FILENO, G.msg_ok, sizeof("NNN " MSG_OK) - 1);
198	if (G.verbose > 1)
199		verbose_log(G.msg_ok);
200}
201#define WRITE_OK(a) cmdio_write_ok(STRNUM32sp(a))
202
203/* TODO: output strerr(errno) if errno != 0? */
204static void
205cmdio_write_error(unsigned status)
206{
207	*(uint32_t *) G.msg_err = status;
208	xwrite(STDOUT_FILENO, G.msg_err, sizeof("NNN " MSG_ERR) - 1);
209	if (G.verbose > 1)
210		verbose_log(G.msg_err);
211}
212#define WRITE_ERR(a) cmdio_write_error(STRNUM32sp(a))
213
214static void
215cmdio_write_raw(const char *p_text)
216{
217	xwrite_str(STDOUT_FILENO, p_text);
218	if (G.verbose > 1)
219		verbose_log(p_text);
220}
221
222static void
223timeout_handler(int sig UNUSED_PARAM)
224{
225	off_t pos;
226	int sv_errno = errno;
227
228	if ((int)(monotonic_sec() - G.end_time) >= 0)
229		goto timed_out;
230
231	if (!G.local_file_fd)
232		goto timed_out;
233
234	pos = xlseek(G.local_file_fd, 0, SEEK_CUR);
235	if (pos == G.local_file_pos)
236		goto timed_out;
237	G.local_file_pos = pos;
238
239	alarm(G.timeout);
240	errno = sv_errno;
241	return;
242
243 timed_out:
244	cmdio_write_raw(STR(FTP_TIMEOUT)" Timeout\r\n");
245/* TODO: do we need to abort (as opposed to usual shutdown) data transfer? */
246	exit(1);
247}
248
249/* Simple commands */
250
251static void
252handle_pwd(void)
253{
254	char *cwd, *response;
255
256	cwd = xrealloc_getcwd_or_warn(NULL);
257	if (cwd == NULL)
258		cwd = xstrdup("");
259
260	/* We have to promote each " to "" */
261	response = escape_text(" \"", cwd, ('"' << 8) + '"');
262	free(cwd);
263	cmdio_write(STRNUM32(FTP_PWDOK), response);
264	free(response);
265}
266
267static void
268handle_cwd(void)
269{
270	if (!G.ftp_arg || chdir(G.ftp_arg) != 0) {
271		WRITE_ERR(FTP_FILEFAIL);
272		return;
273	}
274	WRITE_OK(FTP_CWDOK);
275}
276
277static void
278handle_cdup(void)
279{
280	G.ftp_arg = (char*)"..";
281	handle_cwd();
282}
283
284static void
285handle_stat(void)
286{
287	cmdio_write_raw(STR(FTP_STATOK)"-Server status:\r\n"
288			" TYPE: BINARY\r\n"
289			STR(FTP_STATOK)" Ok\r\n");
290}
291
292/* Examples of HELP and FEAT:
293# nc -vvv ftp.kernel.org 21
294ftp.kernel.org (130.239.17.4:21) open
295220 Welcome to ftp.kernel.org.
296FEAT
297211-Features:
298 EPRT
299 EPSV
300 MDTM
301 PASV
302 REST STREAM
303 SIZE
304 TVFS
305 UTF8
306211 End
307HELP
308214-The following commands are recognized.
309 ABOR ACCT ALLO APPE CDUP CWD  DELE EPRT EPSV FEAT HELP LIST MDTM MKD
310 MODE NLST NOOP OPTS PASS PASV PORT PWD  QUIT REIN REST RETR RMD  RNFR
311 RNTO SITE SIZE SMNT STAT STOR STOU STRU SYST TYPE USER XCUP XCWD XMKD
312 XPWD XRMD
313214 Help OK.
314*/
315static void
316handle_feat(unsigned status)
317{
318	cmdio_write(status, "-Features:");
319	cmdio_write_raw(" EPSV\r\n"
320			" PASV\r\n"
321			" REST STREAM\r\n"
322			" MDTM\r\n"
323			" SIZE\r\n");
324	cmdio_write(status, " Ok");
325}
326
327/* Download commands */
328
329static inline int
330port_active(void)
331{
332	return (G.port_addr != NULL);
333}
334
335static inline int
336pasv_active(void)
337{
338	return (G.pasv_listen_fd > STDOUT_FILENO);
339}
340
341static void
342port_pasv_cleanup(void)
343{
344	free(G.port_addr);
345	G.port_addr = NULL;
346	if (G.pasv_listen_fd > STDOUT_FILENO)
347		close(G.pasv_listen_fd);
348	G.pasv_listen_fd = -1;
349}
350
351/* On error, emits error code to the peer */
352static int
353ftpdataio_get_pasv_fd(void)
354{
355	int remote_fd;
356
357	remote_fd = accept(G.pasv_listen_fd, NULL, 0);
358
359	if (remote_fd < 0) {
360		WRITE_ERR(FTP_BADSENDCONN);
361		return remote_fd;
362	}
363
364	setsockopt(remote_fd, SOL_SOCKET, SO_KEEPALIVE, &const_int_1, sizeof(const_int_1));
365	return remote_fd;
366}
367
368/* Clears port/pasv data.
369 * This means we dont waste resources, for example, keeping
370 * PASV listening socket open when it is no longer needed.
371 * On error, emits error code to the peer (or exits).
372 * On success, emits p_status_msg to the peer.
373 */
374static int
375get_remote_transfer_fd(const char *p_status_msg)
376{
377	int remote_fd;
378
379	if (pasv_active())
380		/* On error, emits error code to the peer */
381		remote_fd = ftpdataio_get_pasv_fd();
382	else
383		/* Exits on error */
384		remote_fd = xconnect_stream(G.port_addr);
385
386	port_pasv_cleanup();
387
388	if (remote_fd < 0)
389		return remote_fd;
390
391	cmdio_write(STRNUM32(FTP_DATACONN), p_status_msg);
392	return remote_fd;
393}
394
395/* If there were neither PASV nor PORT, emits error code to the peer */
396static int
397port_or_pasv_was_seen(void)
398{
399	if (!pasv_active() && !port_active()) {
400		cmdio_write_raw(STR(FTP_BADSENDCONN)" Use PORT/PASV first\r\n");
401		return 0;
402	}
403
404	return 1;
405}
406
407/* Exits on error */
408static unsigned
409bind_for_passive_mode(void)
410{
411	int fd;
412	unsigned port;
413
414	port_pasv_cleanup();
415
416	G.pasv_listen_fd = fd = xsocket(G.local_addr->u.sa.sa_family, SOCK_STREAM, 0);
417	setsockopt_reuseaddr(fd);
418
419	set_nport(G.local_addr, 0);
420	xbind(fd, &G.local_addr->u.sa, G.local_addr->len);
421	xlisten(fd, 1);
422	getsockname(fd, &G.local_addr->u.sa, &G.local_addr->len);
423
424	port = get_nport(&G.local_addr->u.sa);
425	port = ntohs(port);
426	return port;
427}
428
429/* Exits on error */
430static void
431handle_pasv(void)
432{
433	unsigned port;
434	char *addr, *response;
435
436	port = bind_for_passive_mode();
437
438	if (G.local_addr->u.sa.sa_family == AF_INET)
439		addr = xmalloc_sockaddr2dotted_noport(&G.local_addr->u.sa);
440	else /* seen this in the wild done by other ftp servers: */
441		addr = xstrdup("0.0.0.0");
442	replace_char(addr, '.', ',');
443
444	response = xasprintf(STR(FTP_PASVOK)" PASV ok (%s,%u,%u)\r\n",
445			addr, (int)(port >> 8), (int)(port & 255));
446	free(addr);
447	cmdio_write_raw(response);
448	free(response);
449}
450
451/* Exits on error */
452static void
453handle_epsv(void)
454{
455	unsigned port;
456	char *response;
457
458	port = bind_for_passive_mode();
459	response = xasprintf(STR(FTP_EPSVOK)" EPSV ok (|||%u|)\r\n", port);
460	cmdio_write_raw(response);
461	free(response);
462}
463
464static void
465handle_port(void)
466{
467	unsigned port, port_hi;
468	char *raw, *comma;
469#ifdef WHY_BOTHER_WE_CAN_ASSUME_IP_MATCHES
470	socklen_t peer_ipv4_len;
471	struct sockaddr_in peer_ipv4;
472	struct in_addr port_ipv4_sin_addr;
473#endif
474
475	port_pasv_cleanup();
476
477	raw = G.ftp_arg;
478
479	/* PORT command format makes sense only over IPv4 */
480	if (!raw
481#ifdef WHY_BOTHER_WE_CAN_ASSUME_IP_MATCHES
482	 || G.local_addr->u.sa.sa_family != AF_INET
483#endif
484	) {
485 bail:
486		WRITE_ERR(FTP_BADCMD);
487		return;
488	}
489
490	comma = strrchr(raw, ',');
491	if (comma == NULL)
492		goto bail;
493	*comma = '\0';
494	port = bb_strtou(&comma[1], NULL, 10);
495	if (errno || port > 0xff)
496		goto bail;
497
498	comma = strrchr(raw, ',');
499	if (comma == NULL)
500		goto bail;
501	*comma = '\0';
502	port_hi = bb_strtou(&comma[1], NULL, 10);
503	if (errno || port_hi > 0xff)
504		goto bail;
505	port |= port_hi << 8;
506
507#ifdef WHY_BOTHER_WE_CAN_ASSUME_IP_MATCHES
508	replace_char(raw, ',', '.');
509
510	/* We are verifying that PORT's IP matches getpeername().
511	 * Otherwise peer can make us open data connections
512	 * to other hosts (security problem!)
513	 * This code would be too simplistic:
514	 * lsa = xdotted2sockaddr(raw, port);
515	 * if (lsa == NULL) goto bail;
516	 */
517	if (!inet_aton(raw, &port_ipv4_sin_addr))
518		goto bail;
519	peer_ipv4_len = sizeof(peer_ipv4);
520	if (getpeername(STDIN_FILENO, &peer_ipv4, &peer_ipv4_len) != 0)
521		goto bail;
522	if (memcmp(&port_ipv4_sin_addr, &peer_ipv4.sin_addr, sizeof(struct in_addr)) != 0)
523		goto bail;
524
525	G.port_addr = xdotted2sockaddr(raw, port);
526#else
527	G.port_addr = get_peer_lsa(STDIN_FILENO);
528	set_nport(G.port_addr, htons(port));
529#endif
530	WRITE_OK(FTP_PORTOK);
531}
532
533static void
534handle_rest(void)
535{
536	/* When ftp_arg == NULL simply restart from beginning */
537	G.restart_pos = G.ftp_arg ? xatoi_u(G.ftp_arg) : 0;
538	WRITE_OK(FTP_RESTOK);
539}
540
541static void
542handle_retr(void)
543{
544	struct stat statbuf;
545	off_t bytes_transferred;
546	int remote_fd;
547	int local_file_fd;
548	off_t offset = G.restart_pos;
549	char *response;
550
551	G.restart_pos = 0;
552
553	if (!port_or_pasv_was_seen())
554		return; /* port_or_pasv_was_seen emitted error response */
555
556	/* O_NONBLOCK is useful if file happens to be a device node */
557	local_file_fd = G.ftp_arg ? open(G.ftp_arg, O_RDONLY | O_NONBLOCK) : -1;
558	if (local_file_fd < 0) {
559		WRITE_ERR(FTP_FILEFAIL);
560		return;
561	}
562
563	if (fstat(local_file_fd, &statbuf) != 0 || !S_ISREG(statbuf.st_mode)) {
564		/* Note - pretend open failed */
565		WRITE_ERR(FTP_FILEFAIL);
566		goto file_close_out;
567	}
568	G.local_file_fd = local_file_fd;
569
570	/* Now deactive O_NONBLOCK, otherwise we have a problem
571	 * on DMAPI filesystems such as XFS DMAPI.
572	 */
573	ndelay_off(local_file_fd);
574
575	/* Set the download offset (from REST) if any */
576	if (offset != 0)
577		xlseek(local_file_fd, offset, SEEK_SET);
578
579	response = xasprintf(
580		" Opening BINARY connection for %s (%"OFF_FMT"u bytes)",
581		G.ftp_arg, statbuf.st_size);
582	remote_fd = get_remote_transfer_fd(response);
583	free(response);
584	if (remote_fd < 0)
585		goto file_close_out;
586
587	bytes_transferred = bb_copyfd_eof(local_file_fd, remote_fd);
588	close(remote_fd);
589	if (bytes_transferred < 0)
590		WRITE_ERR(FTP_BADSENDFILE);
591	else
592		WRITE_OK(FTP_TRANSFEROK);
593
594 file_close_out:
595	close(local_file_fd);
596	G.local_file_fd = 0;
597}
598
599/* List commands */
600
601static int
602popen_ls(const char *opt)
603{
604	const char *argv[5];
605	struct fd_pair outfd;
606	pid_t pid;
607
608	argv[0] = "ftpd";
609	argv[1] = opt; /* "-l" or "-1" */
610#if BB_MMU
611	argv[2] = "--";
612#else
613	/* NOMMU ftpd ls helper chdirs to argv[2],
614	 * preventing peer from seeing real root. */
615	argv[2] = xrealloc_getcwd_or_warn(NULL);
616#endif
617	argv[3] = G.ftp_arg;
618	argv[4] = NULL;
619
620	/* Improve compatibility with non-RFC conforming FTP clients
621	 * which send e.g. "LIST -l", "LIST -la", "LIST -aL".
622	 * See https://bugs.kde.org/show_bug.cgi?id=195578 */
623	if (ENABLE_FEATURE_FTPD_ACCEPT_BROKEN_LIST
624	 && G.ftp_arg && G.ftp_arg[0] == '-'
625	) {
626		const char *tmp = strchr(G.ftp_arg, ' ');
627		if (tmp) /* skip the space */
628			tmp++;
629		argv[3] = tmp;
630	}
631
632	xpiped_pair(outfd);
633
634	/*fflush_all(); - so far we dont use stdio on output */
635	pid = BB_MMU ? xfork() : xvfork();
636	if (pid == 0) {
637		/* child */
638#if !BB_MMU
639		/* On NOMMU, we want to execute a child - copy of ourself.
640		 * In chroot we usually can't do it. Thus we chdir
641		 * out of the chroot back to original root,
642		 * and (see later below) execute bb_busybox_exec_path
643		 * relative to current directory */
644		if (fchdir(G.root_fd) != 0)
645			_exit(127);
646		/*close(G.root_fd); - close_on_exec_on() took care of this */
647#endif
648		/* NB: close _first_, then move fd! */
649		close(outfd.rd);
650		xmove_fd(outfd.wr, STDOUT_FILENO);
651		/* Opening /dev/null in chroot is hard.
652		 * Just making sure STDIN_FILENO is opened
653		 * to something harmless. Paranoia,
654		 * ls won't read it anyway */
655		close(STDIN_FILENO);
656		dup(STDOUT_FILENO); /* copy will become STDIN_FILENO */
657#if BB_MMU
658		/* memset(&G, 0, sizeof(G)); - ls_main does it */
659		exit(ls_main(ARRAY_SIZE(argv) - 1, (char**) argv));
660#else
661		/* + 1: we must use relative path here if in chroot.
662		 * For example, execv("/proc/self/exe") will fail, since
663		 * it looks for "/proc/self/exe" _relative to chroot!_ */
664		execv(bb_busybox_exec_path + 1, (char**) argv);
665		_exit(127);
666#endif
667	}
668
669	/* parent */
670	close(outfd.wr);
671#if !BB_MMU
672	free((char*)argv[2]);
673#endif
674	return outfd.rd;
675}
676
677enum {
678	USE_CTRL_CONN = 1,
679	LONG_LISTING = 2,
680};
681
682static void
683handle_dir_common(int opts)
684{
685	FILE *ls_fp;
686	char *line;
687	int ls_fd;
688
689	if (!(opts & USE_CTRL_CONN) && !port_or_pasv_was_seen())
690		return; /* port_or_pasv_was_seen emitted error response */
691
692	/* -n prevents user/groupname display,
693	 * which can be problematic in chroot */
694	ls_fd = popen_ls((opts & LONG_LISTING) ? "-l" : "-1");
695	ls_fp = xfdopen_for_read(ls_fd);
696
697	if (opts & USE_CTRL_CONN) {
698		/* STAT <filename> */
699		cmdio_write_raw(STR(FTP_STATFILE_OK)"-File status:\r\n");
700		while (1) {
701			line = xmalloc_fgetline(ls_fp);
702			if (!line)
703				break;
704			/* Hack: 0 results in no status at all */
705			/* Note: it's ok that we don't prepend space,
706			 * ftp.kernel.org doesn't do that too */
707			cmdio_write(0, line);
708			free(line);
709		}
710		WRITE_OK(FTP_STATFILE_OK);
711	} else {
712		/* LIST/NLST [<filename>] */
713		int remote_fd = get_remote_transfer_fd(" Directory listing");
714		if (remote_fd >= 0) {
715			while (1) {
716				line = xmalloc_fgetline(ls_fp);
717				if (!line)
718					break;
719				/* I've seen clients complaining when they
720				 * are fed with ls output with bare '\n'.
721				 * Pity... that would be much simpler.
722				 */
723/* TODO: need to s/LF/NUL/g here */
724				xwrite_str(remote_fd, line);
725				xwrite(remote_fd, "\r\n", 2);
726				free(line);
727			}
728		}
729		close(remote_fd);
730		WRITE_OK(FTP_TRANSFEROK);
731	}
732	fclose(ls_fp); /* closes ls_fd too */
733}
734static void
735handle_list(void)
736{
737	handle_dir_common(LONG_LISTING);
738}
739static void
740handle_nlst(void)
741{
742	/* NLST returns list of names, "\r\n" terminated without regard
743	 * to the current binary flag. Names may start with "/",
744	 * then they represent full names (we don't produce such names),
745	 * otherwise names are relative to current directory.
746	 * Embedded "\n" are replaced by NULs. This is safe since names
747	 * can never contain NUL.
748	 */
749	handle_dir_common(0);
750}
751static void
752handle_stat_file(void)
753{
754	handle_dir_common(LONG_LISTING + USE_CTRL_CONN);
755}
756
757/* This can be extended to handle MLST, as all info is available
758 * in struct stat for that:
759 * MLST file_name
760 * 250-Listing file_name
761 *  type=file;size=4161;modify=19970214165800; /dir/dir/file_name
762 * 250 End
763 * Nano-doc:
764 * MLST [<file or dir name, "." assumed if not given>]
765 * Returned name should be either the same as requested, or fully qualified.
766 * If there was no parameter, return "" or (preferred) fully-qualified name.
767 * Returned "facts" (case is not important):
768 *  size    - size in octets
769 *  modify  - last modification time
770 *  type    - entry type (file,dir,OS.unix=block)
771 *            (+ cdir and pdir types for MLSD)
772 *  unique  - unique id of file/directory (inode#)
773 *  perm    -
774 *      a: can be appended to (APPE)
775 *      d: can be deleted (RMD/DELE)
776 *      f: can be renamed (RNFR)
777 *      r: can be read (RETR)
778 *      w: can be written (STOR)
779 *      e: can CWD into this dir
780 *      l: this dir can be listed (dir only!)
781 *      c: can create files in this dir
782 *      m: can create dirs in this dir (MKD)
783 *      p: can delete files in this dir
784 *  UNIX.mode - unix file mode
785 */
786static void
787handle_size_or_mdtm(int need_size)
788{
789	struct stat statbuf;
790	struct tm broken_out;
791	char buf[(sizeof("NNN %"OFF_FMT"u\r\n") + sizeof(off_t) * 3)
792		| sizeof("NNN YYYYMMDDhhmmss\r\n")
793	];
794
795	if (!G.ftp_arg
796	 || stat(G.ftp_arg, &statbuf) != 0
797	 || !S_ISREG(statbuf.st_mode)
798	) {
799		WRITE_ERR(FTP_FILEFAIL);
800		return;
801	}
802	if (need_size) {
803		sprintf(buf, STR(FTP_STATFILE_OK)" %"OFF_FMT"u\r\n", statbuf.st_size);
804	} else {
805		gmtime_r(&statbuf.st_mtime, &broken_out);
806		sprintf(buf, STR(FTP_STATFILE_OK)" %04u%02u%02u%02u%02u%02u\r\n",
807			broken_out.tm_year + 1900,
808			broken_out.tm_mon,
809			broken_out.tm_mday,
810			broken_out.tm_hour,
811			broken_out.tm_min,
812			broken_out.tm_sec);
813	}
814	cmdio_write_raw(buf);
815}
816
817/* Upload commands */
818
819#if ENABLE_FEATURE_FTP_WRITE
820static void
821handle_mkd(void)
822{
823	if (!G.ftp_arg || mkdir(G.ftp_arg, 0777) != 0) {
824		WRITE_ERR(FTP_FILEFAIL);
825		return;
826	}
827	WRITE_OK(FTP_MKDIROK);
828}
829
830static void
831handle_rmd(void)
832{
833	if (!G.ftp_arg || rmdir(G.ftp_arg) != 0) {
834		WRITE_ERR(FTP_FILEFAIL);
835		return;
836	}
837	WRITE_OK(FTP_RMDIROK);
838}
839
840static void
841handle_dele(void)
842{
843	if (!G.ftp_arg || unlink(G.ftp_arg) != 0) {
844		WRITE_ERR(FTP_FILEFAIL);
845		return;
846	}
847	WRITE_OK(FTP_DELEOK);
848}
849
850static void
851handle_rnfr(void)
852{
853	free(G.rnfr_filename);
854	G.rnfr_filename = xstrdup(G.ftp_arg);
855	WRITE_OK(FTP_RNFROK);
856}
857
858static void
859handle_rnto(void)
860{
861	int retval;
862
863	/* If we didn't get a RNFR, throw a wobbly */
864	if (G.rnfr_filename == NULL || G.ftp_arg == NULL) {
865		cmdio_write_raw(STR(FTP_NEEDRNFR)" Use RNFR first\r\n");
866		return;
867	}
868
869	retval = rename(G.rnfr_filename, G.ftp_arg);
870	free(G.rnfr_filename);
871	G.rnfr_filename = NULL;
872
873	if (retval) {
874		WRITE_ERR(FTP_FILEFAIL);
875		return;
876	}
877	WRITE_OK(FTP_RENAMEOK);
878}
879
880static void
881handle_upload_common(int is_append, int is_unique)
882{
883	struct stat statbuf;
884	char *tempname;
885	off_t bytes_transferred;
886	off_t offset;
887	int local_file_fd;
888	int remote_fd;
889
890	offset = G.restart_pos;
891	G.restart_pos = 0;
892
893	if (!port_or_pasv_was_seen())
894		return; /* port_or_pasv_was_seen emitted error response */
895
896	tempname = NULL;
897	local_file_fd = -1;
898	if (is_unique) {
899		tempname = xstrdup(" FILE: uniq.XXXXXX");
900		local_file_fd = mkstemp(tempname + 7);
901	} else if (G.ftp_arg) {
902		int flags = O_WRONLY | O_CREAT | O_TRUNC;
903		if (is_append)
904			flags = O_WRONLY | O_CREAT | O_APPEND;
905		if (offset)
906			flags = O_WRONLY | O_CREAT;
907		local_file_fd = open(G.ftp_arg, flags, 0666);
908	}
909
910	if (local_file_fd < 0
911	 || fstat(local_file_fd, &statbuf) != 0
912	 || !S_ISREG(statbuf.st_mode)
913	) {
914		WRITE_ERR(FTP_UPLOADFAIL);
915		if (local_file_fd >= 0)
916			goto close_local_and_bail;
917		return;
918	}
919	G.local_file_fd = local_file_fd;
920
921	if (offset)
922		xlseek(local_file_fd, offset, SEEK_SET);
923
924	remote_fd = get_remote_transfer_fd(tempname ? tempname : " Ok to send data");
925	free(tempname);
926
927	if (remote_fd < 0)
928		goto close_local_and_bail;
929
930	bytes_transferred = bb_copyfd_eof(remote_fd, local_file_fd);
931	close(remote_fd);
932	if (bytes_transferred < 0)
933		WRITE_ERR(FTP_BADSENDFILE);
934	else
935		WRITE_OK(FTP_TRANSFEROK);
936
937 close_local_and_bail:
938	close(local_file_fd);
939	G.local_file_fd = 0;
940}
941
942static void
943handle_stor(void)
944{
945	handle_upload_common(0, 0);
946}
947
948static void
949handle_appe(void)
950{
951	G.restart_pos = 0;
952	handle_upload_common(1, 0);
953}
954
955static void
956handle_stou(void)
957{
958	G.restart_pos = 0;
959	handle_upload_common(0, 1);
960}
961#endif /* ENABLE_FEATURE_FTP_WRITE */
962
963static uint32_t
964cmdio_get_cmd_and_arg(void)
965{
966	int len;
967	uint32_t cmdval;
968	char *cmd;
969
970	alarm(G.timeout);
971
972	free(G.ftp_cmd);
973	{
974		/* Paranoia. Peer may send 1 gigabyte long cmd... */
975		/* Using separate len_on_stk instead of len optimizes
976		 * code size (allows len to be in CPU register) */
977		size_t len_on_stk = 8 * 1024;
978		G.ftp_cmd = cmd = xmalloc_fgets_str_len(stdin, "\r\n", &len_on_stk);
979		if (!cmd)
980			exit(0);
981		len = len_on_stk;
982	}
983
984	/* De-escape telnet: 0xff,0xff => 0xff */
985	/* RFC959 says that ABOR, STAT, QUIT may be sent even during
986	 * data transfer, and may be preceded by telnet's "Interrupt Process"
987	 * code (two-byte sequence 255,244) and then by telnet "Synch" code
988	 * 255,242 (byte 242 is sent with TCP URG bit using send(MSG_OOB)
989	 * and may generate SIGURG on our side. See RFC854).
990	 * So far we don't support that (may install SIGURG handler if we'd want to),
991	 * but we need to at least remove 255,xxx pairs. lftp sends those. */
992	/* Then de-escape FTP: NUL => '\n' */
993	/* Testing for \xff:
994	 * Create file named '\xff': echo Hello >`echo -ne "\xff"`
995	 * Try to get it:            ftpget -v 127.0.0.1 Eff `echo -ne "\xff\xff"`
996	 * (need "\xff\xff" until ftpget applet is fixed to do escaping :)
997	 * Testing for embedded LF:
998	 * LF_HERE=`echo -ne "LF\nHERE"`
999	 * echo Hello >"$LF_HERE"
1000	 * ftpget -v 127.0.0.1 LF_HERE "$LF_HERE"
1001	 */
1002	{
1003		int dst, src;
1004
1005		/* Strip "\r\n" if it is there */
1006		if (len != 0 && cmd[len - 1] == '\n') {
1007			len--;
1008			if (len != 0 && cmd[len - 1] == '\r')
1009				len--;
1010			cmd[len] = '\0';
1011		}
1012		src = strchrnul(cmd, 0xff) - cmd;
1013		/* 99,99% there are neither NULs nor 255s and src == len */
1014		if (src < len) {
1015			dst = src;
1016			do {
1017				if ((unsigned char)(cmd[src]) == 255) {
1018					src++;
1019					/* 255,xxx - skip 255 */
1020					if ((unsigned char)(cmd[src]) != 255) {
1021						/* 255,!255 - skip both */
1022						src++;
1023						continue;
1024					}
1025					/* 255,255 - retain one 255 */
1026				}
1027				/* NUL => '\n' */
1028				cmd[dst++] = cmd[src] ? cmd[src] : '\n';
1029				src++;
1030			} while (src < len);
1031			cmd[dst] = '\0';
1032		}
1033	}
1034
1035	if (G.verbose > 1)
1036		verbose_log(cmd);
1037
1038	G.ftp_arg = strchr(cmd, ' ');
1039	if (G.ftp_arg != NULL)
1040		*G.ftp_arg++ = '\0';
1041
1042	/* Uppercase and pack into uint32_t first word of the command */
1043	cmdval = 0;
1044	while (*cmd)
1045		cmdval = (cmdval << 8) + ((unsigned char)*cmd++ & (unsigned char)~0x20);
1046
1047	return cmdval;
1048}
1049
1050#define mk_const4(a,b,c,d) (((a * 0x100 + b) * 0x100 + c) * 0x100 + d)
1051#define mk_const3(a,b,c)    ((a * 0x100 + b) * 0x100 + c)
1052enum {
1053	const_ALLO = mk_const4('A', 'L', 'L', 'O'),
1054	const_APPE = mk_const4('A', 'P', 'P', 'E'),
1055	const_CDUP = mk_const4('C', 'D', 'U', 'P'),
1056	const_CWD  = mk_const3('C', 'W', 'D'),
1057	const_DELE = mk_const4('D', 'E', 'L', 'E'),
1058	const_EPSV = mk_const4('E', 'P', 'S', 'V'),
1059	const_FEAT = mk_const4('F', 'E', 'A', 'T'),
1060	const_HELP = mk_const4('H', 'E', 'L', 'P'),
1061	const_LIST = mk_const4('L', 'I', 'S', 'T'),
1062	const_MDTM = mk_const4('M', 'D', 'T', 'M'),
1063	const_MKD  = mk_const3('M', 'K', 'D'),
1064	const_MODE = mk_const4('M', 'O', 'D', 'E'),
1065	const_NLST = mk_const4('N', 'L', 'S', 'T'),
1066	const_NOOP = mk_const4('N', 'O', 'O', 'P'),
1067	const_PASS = mk_const4('P', 'A', 'S', 'S'),
1068	const_PASV = mk_const4('P', 'A', 'S', 'V'),
1069	const_PORT = mk_const4('P', 'O', 'R', 'T'),
1070	const_PWD  = mk_const3('P', 'W', 'D'),
1071	const_QUIT = mk_const4('Q', 'U', 'I', 'T'),
1072	const_REST = mk_const4('R', 'E', 'S', 'T'),
1073	const_RETR = mk_const4('R', 'E', 'T', 'R'),
1074	const_RMD  = mk_const3('R', 'M', 'D'),
1075	const_RNFR = mk_const4('R', 'N', 'F', 'R'),
1076	const_RNTO = mk_const4('R', 'N', 'T', 'O'),
1077	const_SIZE = mk_const4('S', 'I', 'Z', 'E'),
1078	const_STAT = mk_const4('S', 'T', 'A', 'T'),
1079	const_STOR = mk_const4('S', 'T', 'O', 'R'),
1080	const_STOU = mk_const4('S', 'T', 'O', 'U'),
1081	const_STRU = mk_const4('S', 'T', 'R', 'U'),
1082	const_SYST = mk_const4('S', 'Y', 'S', 'T'),
1083	const_TYPE = mk_const4('T', 'Y', 'P', 'E'),
1084	const_USER = mk_const4('U', 'S', 'E', 'R'),
1085
1086#if !BB_MMU
1087	OPT_l = (1 << 0),
1088	OPT_1 = (1 << 1),
1089#endif
1090	OPT_v = (1 << ((!BB_MMU) * 2 + 0)),
1091	OPT_S = (1 << ((!BB_MMU) * 2 + 1)),
1092	OPT_w = (1 << ((!BB_MMU) * 2 + 2)) * ENABLE_FEATURE_FTP_WRITE,
1093};
1094
1095int ftpd_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
1096#if !BB_MMU
1097int ftpd_main(int argc, char **argv)
1098#else
1099int ftpd_main(int argc UNUSED_PARAM, char **argv)
1100#endif
1101{
1102	unsigned abs_timeout;
1103	unsigned verbose_S;
1104	smallint opts;
1105
1106	INIT_G();
1107
1108	abs_timeout = 1 * 60 * 60;
1109	verbose_S = 0;
1110	G.timeout = 2 * 60;
1111	opt_complementary = "t+:T+:vv:SS";
1112#if BB_MMU
1113	opts = getopt32(argv,   "vS" IF_FEATURE_FTP_WRITE("w") "t:T:", &G.timeout, &abs_timeout, &G.verbose, &verbose_S);
1114#else
1115	opts = getopt32(argv, "l1vS" IF_FEATURE_FTP_WRITE("w") "t:T:", &G.timeout, &abs_timeout, &G.verbose, &verbose_S);
1116	if (opts & (OPT_l|OPT_1)) {
1117		/* Our secret backdoor to ls */
1118/* TODO: pass -n? It prevents user/group resolution, which may not work in chroot anyway */
1119/* TODO: pass -A? It shows dot files */
1120/* TODO: pass --group-directories-first? would be nice, but ls doesn't do that yet */
1121		xchdir(argv[2]);
1122		argv[2] = (char*)"--";
1123		/* memset(&G, 0, sizeof(G)); - ls_main does it */
1124		return ls_main(argc, argv);
1125	}
1126#endif
1127	if (G.verbose < verbose_S)
1128		G.verbose = verbose_S;
1129	if (abs_timeout | G.timeout) {
1130		if (abs_timeout == 0)
1131			abs_timeout = INT_MAX;
1132		G.end_time = monotonic_sec() + abs_timeout;
1133		if (G.timeout > abs_timeout)
1134			G.timeout = abs_timeout;
1135	}
1136	strcpy(G.msg_ok  + 4, MSG_OK );
1137	strcpy(G.msg_err + 4, MSG_ERR);
1138
1139	G.local_addr = get_sock_lsa(STDIN_FILENO);
1140	if (!G.local_addr) {
1141		/* This is confusing:
1142		 * bb_error_msg_and_die("stdin is not a socket");
1143		 * Better: */
1144		bb_show_usage();
1145		/* Help text says that ftpd must be used as inetd service,
1146		 * which is by far the most usual cause of get_sock_lsa
1147		 * failure */
1148	}
1149
1150	if (!(opts & OPT_v))
1151		logmode = LOGMODE_NONE;
1152	if (opts & OPT_S) {
1153		/* LOG_NDELAY is needed since we may chroot later */
1154		openlog(applet_name, LOG_PID | LOG_NDELAY, LOG_DAEMON);
1155		logmode |= LOGMODE_SYSLOG;
1156	}
1157	if (logmode)
1158		applet_name = xasprintf("%s[%u]", applet_name, (int)getpid());
1159
1160#if !BB_MMU
1161	G.root_fd = xopen("/", O_RDONLY | O_DIRECTORY);
1162	close_on_exec_on(G.root_fd);
1163#endif
1164
1165	if (argv[optind]) {
1166		xchdir(argv[optind]);
1167		chroot(".");
1168	}
1169
1170	//umask(077); - admin can set umask before starting us
1171
1172	/* Signals. We'll always take -EPIPE rather than a rude signal, thanks */
1173	signal(SIGPIPE, SIG_IGN);
1174
1175	/* Set up options on the command socket (do we need these all? why?) */
1176	setsockopt(STDIN_FILENO, IPPROTO_TCP, TCP_NODELAY, &const_int_1, sizeof(const_int_1));
1177	setsockopt(STDIN_FILENO, SOL_SOCKET, SO_KEEPALIVE, &const_int_1, sizeof(const_int_1));
1178	/* Telnet protocol over command link may send "urgent" data,
1179	 * we prefer it to be received in the "normal" data stream: */
1180	setsockopt(STDIN_FILENO, SOL_SOCKET, SO_OOBINLINE, &const_int_1, sizeof(const_int_1));
1181
1182	WRITE_OK(FTP_GREET);
1183	signal(SIGALRM, timeout_handler);
1184
1185#ifdef IF_WE_WANT_TO_REQUIRE_LOGIN
1186	{
1187		smallint user_was_specified = 0;
1188		while (1) {
1189			uint32_t cmdval = cmdio_get_cmd_and_arg();
1190
1191			if (cmdval == const_USER) {
1192				if (G.ftp_arg == NULL || strcasecmp(G.ftp_arg, "anonymous") != 0)
1193					cmdio_write_raw(STR(FTP_LOGINERR)" Server is anonymous only\r\n");
1194				else {
1195					user_was_specified = 1;
1196					cmdio_write_raw(STR(FTP_GIVEPWORD)" Please specify the password\r\n");
1197				}
1198			} else if (cmdval == const_PASS) {
1199				if (user_was_specified)
1200					break;
1201				cmdio_write_raw(STR(FTP_NEEDUSER)" Login with USER\r\n");
1202			} else if (cmdval == const_QUIT) {
1203				WRITE_OK(FTP_GOODBYE);
1204				return 0;
1205			} else {
1206				cmdio_write_raw(STR(FTP_LOGINERR)" Login with USER and PASS\r\n");
1207			}
1208		}
1209	}
1210	WRITE_OK(FTP_LOGINOK);
1211#endif
1212
1213	/* RFC-959 Section 5.1
1214	 * The following commands and options MUST be supported by every
1215	 * server-FTP and user-FTP, except in cases where the underlying
1216	 * file system or operating system does not allow or support
1217	 * a particular command.
1218	 * Type: ASCII Non-print, IMAGE, LOCAL 8
1219	 * Mode: Stream
1220	 * Structure: File, Record*
1221	 * (Record structure is REQUIRED only for hosts whose file
1222	 *  systems support record structure).
1223	 * Commands:
1224	 * USER, PASS, ACCT, [bbox: ACCT not supported]
1225	 * PORT, PASV,
1226	 * TYPE, MODE, STRU,
1227	 * RETR, STOR, APPE,
1228	 * RNFR, RNTO, DELE,
1229	 * CWD,  CDUP, RMD,  MKD,  PWD,
1230	 * LIST, NLST,
1231	 * SYST, STAT,
1232	 * HELP, NOOP, QUIT.
1233	 */
1234	/* ACCOUNT (ACCT)
1235	 * "The argument field is a Telnet string identifying the user's account.
1236	 * The command is not necessarily related to the USER command, as some
1237	 * sites may require an account for login and others only for specific
1238	 * access, such as storing files. In the latter case the command may
1239	 * arrive at any time.
1240	 * There are reply codes to differentiate these cases for the automation:
1241	 * when account information is required for login, the response to
1242	 * a successful PASSword command is reply code 332. On the other hand,
1243	 * if account information is NOT required for login, the reply to
1244	 * a successful PASSword command is 230; and if the account information
1245	 * is needed for a command issued later in the dialogue, the server
1246	 * should return a 332 or 532 reply depending on whether it stores
1247	 * (pending receipt of the ACCounT command) or discards the command,
1248	 * respectively."
1249	 */
1250
1251	while (1) {
1252		uint32_t cmdval = cmdio_get_cmd_and_arg();
1253
1254		if (cmdval == const_QUIT) {
1255			WRITE_OK(FTP_GOODBYE);
1256			return 0;
1257		}
1258		else if (cmdval == const_USER)
1259			/* This would mean "ok, now give me PASS". */
1260			/*WRITE_OK(FTP_GIVEPWORD);*/
1261			/* vsftpd can be configured to not require that,
1262			 * and this also saves one roundtrip:
1263			 */
1264			WRITE_OK(FTP_LOGINOK);
1265		else if (cmdval == const_PASS)
1266			WRITE_OK(FTP_LOGINOK);
1267		else if (cmdval == const_NOOP)
1268			WRITE_OK(FTP_NOOPOK);
1269		else if (cmdval == const_TYPE)
1270			WRITE_OK(FTP_TYPEOK);
1271		else if (cmdval == const_STRU)
1272			WRITE_OK(FTP_STRUOK);
1273		else if (cmdval == const_MODE)
1274			WRITE_OK(FTP_MODEOK);
1275		else if (cmdval == const_ALLO)
1276			WRITE_OK(FTP_ALLOOK);
1277		else if (cmdval == const_SYST)
1278			cmdio_write_raw(STR(FTP_SYSTOK)" UNIX Type: L8\r\n");
1279		else if (cmdval == const_PWD)
1280			handle_pwd();
1281		else if (cmdval == const_CWD)
1282			handle_cwd();
1283		else if (cmdval == const_CDUP) /* cd .. */
1284			handle_cdup();
1285		/* HELP is nearly useless, but we can reuse FEAT for it */
1286		/* lftp uses FEAT */
1287		else if (cmdval == const_HELP || cmdval == const_FEAT)
1288			handle_feat(cmdval == const_HELP
1289					? STRNUM32(FTP_HELP)
1290					: STRNUM32(FTP_STATOK)
1291			);
1292		else if (cmdval == const_LIST) /* ls -l */
1293			handle_list();
1294		else if (cmdval == const_NLST) /* "name list", bare ls */
1295			handle_nlst();
1296		/* SIZE is crucial for wget's download indicator etc */
1297		/* Mozilla, lftp use MDTM (presumably for caching) */
1298		else if (cmdval == const_SIZE || cmdval == const_MDTM)
1299			handle_size_or_mdtm(cmdval == const_SIZE);
1300		else if (cmdval == const_STAT) {
1301			if (G.ftp_arg == NULL)
1302				handle_stat();
1303			else
1304				handle_stat_file();
1305		}
1306		else if (cmdval == const_PASV)
1307			handle_pasv();
1308		else if (cmdval == const_EPSV)
1309			handle_epsv();
1310		else if (cmdval == const_RETR)
1311			handle_retr();
1312		else if (cmdval == const_PORT)
1313			handle_port();
1314		else if (cmdval == const_REST)
1315			handle_rest();
1316#if ENABLE_FEATURE_FTP_WRITE
1317		else if (opts & OPT_w) {
1318			if (cmdval == const_STOR)
1319				handle_stor();
1320			else if (cmdval == const_MKD)
1321				handle_mkd();
1322			else if (cmdval == const_RMD)
1323				handle_rmd();
1324			else if (cmdval == const_DELE)
1325				handle_dele();
1326			else if (cmdval == const_RNFR) /* "rename from" */
1327				handle_rnfr();
1328			else if (cmdval == const_RNTO) /* "rename to" */
1329				handle_rnto();
1330			else if (cmdval == const_APPE)
1331				handle_appe();
1332			else if (cmdval == const_STOU) /* "store unique" */
1333				handle_stou();
1334			else
1335				goto bad_cmd;
1336		}
1337#endif
1338#if 0
1339		else if (cmdval == const_STOR
1340		 || cmdval == const_MKD
1341		 || cmdval == const_RMD
1342		 || cmdval == const_DELE
1343		 || cmdval == const_RNFR
1344		 || cmdval == const_RNTO
1345		 || cmdval == const_APPE
1346		 || cmdval == const_STOU
1347		) {
1348			cmdio_write_raw(STR(FTP_NOPERM)" Permission denied\r\n");
1349		}
1350#endif
1351		else {
1352			/* Which unsupported commands were seen in the wild?
1353			 * (doesn't necessarily mean "we must support them")
1354			 * foo 1.2.3: XXXX - comment
1355			 */
1356#if ENABLE_FEATURE_FTP_WRITE
1357 bad_cmd:
1358#endif
1359			cmdio_write_raw(STR(FTP_BADCMD)" Unknown command\r\n");
1360		}
1361	}
1362}
1363