xref: /original-bsd/libexec/ftpd/ftpcmd.y (revision cd18b70b)
1 /*
2  * Copyright (c) 1985 Regents of the University of California.
3  * All rights reserved.  The Berkeley software License Agreement
4  * specifies the terms and conditions for redistribution.
5  */
6 
7 /*
8  * Grammar for FTP commands.
9  * See RFC 765.
10  */
11 
12 %{
13 
14 #ifndef lint
15 static	char sccsid[] = "@(#)ftpcmd.y	5.9 (Berkeley) 05/15/87";
16 #endif
17 
18 #include <sys/types.h>
19 #include <sys/socket.h>
20 
21 #include <netinet/in.h>
22 
23 #include <arpa/ftp.h>
24 
25 #include <stdio.h>
26 #include <signal.h>
27 #include <ctype.h>
28 #include <pwd.h>
29 #include <setjmp.h>
30 #include <syslog.h>
31 
32 extern	struct sockaddr_in data_dest;
33 extern	int logged_in;
34 extern	struct passwd *pw;
35 extern	int guest;
36 extern	int logging;
37 extern	int type;
38 extern	int form;
39 extern	int debug;
40 extern	int timeout;
41 extern  int pdata;
42 extern	char hostname[];
43 extern	char *globerr;
44 extern	int usedefault;
45 extern	int unique;
46 extern  int transflag;
47 extern  char tmpline[];
48 char	**glob();
49 
50 static	int cmd_type;
51 static	int cmd_form;
52 static	int cmd_bytesz;
53 char cbuf[512];
54 char *fromname;
55 
56 char	*index();
57 %}
58 
59 %token
60 	A	B	C	E	F	I
61 	L	N	P	R	S	T
62 
63 	SP	CRLF	COMMA	STRING	NUMBER
64 
65 	USER	PASS	ACCT	REIN	QUIT	PORT
66 	PASV	TYPE	STRU	MODE	RETR	STOR
67 	APPE	MLFL	MAIL	MSND	MSOM	MSAM
68 	MRSQ	MRCP	ALLO	REST	RNFR	RNTO
69 	ABOR	DELE	CWD	LIST	NLST	SITE
70 	STAT	HELP	NOOP	XMKD	XRMD	XPWD
71 	XCUP	STOU
72 
73 	LEXERR
74 
75 %start	cmd_list
76 
77 %%
78 
79 cmd_list:	/* empty */
80 	|	cmd_list cmd
81 		= {
82 			fromname = (char *) 0;
83 		}
84 	|	cmd_list rcmd
85 	;
86 
87 cmd:		USER SP username CRLF
88 		= {
89 			extern struct passwd *getpwnam();
90 
91 			logged_in = 0;
92 			if (strcmp((char *) $3, "ftp") == 0 ||
93 			  strcmp((char *) $3, "anonymous") == 0) {
94 				if ((pw = getpwnam("ftp")) != NULL) {
95 					guest = 1;
96 					reply(331,
97 				  "Guest login ok, send ident as password.");
98 				}
99 				else {
100 					reply(530, "User %s unknown.", $3);
101 				}
102 			} else if (checkuser((char *) $3)) {
103 				guest = 0;
104 				pw = getpwnam((char *) $3);
105 				if (pw == NULL) {
106 					reply(530, "User %s unknown.", $3);
107 				}
108 				else {
109 				    reply(331, "Password required for %s.", $3);
110 				}
111 			} else {
112 				reply(530, "User %s access denied.", $3);
113 			}
114 			free((char *) $3);
115 		}
116 	|	PASS SP password CRLF
117 		= {
118 			pass((char *) $3);
119 			free((char *) $3);
120 		}
121 	|	PORT SP host_port CRLF
122 		= {
123 			usedefault = 0;
124 			if (pdata > 0) {
125 				(void) close(pdata);
126 			}
127 			pdata = -1;
128 			reply(200, "PORT command successful.");
129 		}
130 	|	PASV CRLF
131 		= {
132 			passive();
133 		}
134 	|	TYPE SP type_code CRLF
135 		= {
136 			switch (cmd_type) {
137 
138 			case TYPE_A:
139 				if (cmd_form == FORM_N) {
140 					reply(200, "Type set to A.");
141 					type = cmd_type;
142 					form = cmd_form;
143 				} else
144 					reply(504, "Form must be N.");
145 				break;
146 
147 			case TYPE_E:
148 				reply(504, "Type E not implemented.");
149 				break;
150 
151 			case TYPE_I:
152 				reply(200, "Type set to I.");
153 				type = cmd_type;
154 				break;
155 
156 			case TYPE_L:
157 				if (cmd_bytesz == 8) {
158 					reply(200,
159 					    "Type set to L (byte size 8).");
160 					type = cmd_type;
161 				} else
162 					reply(504, "Byte size must be 8.");
163 			}
164 		}
165 	|	STRU SP struct_code CRLF
166 		= {
167 			switch ($3) {
168 
169 			case STRU_F:
170 				reply(200, "STRU F ok.");
171 				break;
172 
173 			default:
174 				reply(504, "Unimplemented STRU type.");
175 			}
176 		}
177 	|	MODE SP mode_code CRLF
178 		= {
179 			switch ($3) {
180 
181 			case MODE_S:
182 				reply(200, "MODE S ok.");
183 				break;
184 
185 			default:
186 				reply(502, "Unimplemented MODE type.");
187 			}
188 		}
189 	|	ALLO SP NUMBER CRLF
190 		= {
191 			reply(202, "ALLO command ignored.");
192 		}
193 	|	RETR check_login SP pathname CRLF
194 		= {
195 			if ($2 && $4 != NULL)
196 				retrieve((char *) 0, (char *) $4);
197 			if ($4 != NULL)
198 				free((char *) $4);
199 		}
200 	|	STOR check_login SP pathname CRLF
201 		= {
202 			if ($2 && $4 != NULL)
203 				store((char *) $4, "w");
204 			if ($4 != NULL)
205 				free((char *) $4);
206 		}
207 	|	APPE check_login SP pathname CRLF
208 		= {
209 			if ($2 && $4 != NULL)
210 				store((char *) $4, "a");
211 			if ($4 != NULL)
212 				free((char *) $4);
213 		}
214 	|	NLST check_login CRLF
215 		= {
216 			if ($2)
217 				retrieve("/bin/ls", "");
218 		}
219 	|	NLST check_login SP pathname CRLF
220 		= {
221 			if ($2 && $4 != NULL)
222 				retrieve("/bin/ls %s", (char *) $4);
223 			if ($4 != NULL)
224 				free((char *) $4);
225 		}
226 	|	LIST check_login CRLF
227 		= {
228 			if ($2)
229 				retrieve("/bin/ls -lg", "");
230 		}
231 	|	LIST check_login SP pathname CRLF
232 		= {
233 			if ($2 && $4 != NULL)
234 				retrieve("/bin/ls -lg %s", (char *) $4);
235 			if ($4 != NULL)
236 				free((char *) $4);
237 		}
238 	|	DELE check_login SP pathname CRLF
239 		= {
240 			if ($2 && $4 != NULL)
241 				delete((char *) $4);
242 			if ($4 != NULL)
243 				free((char *) $4);
244 		}
245 	|	RNTO SP pathname CRLF
246 		= {
247 			if (fromname) {
248 				renamecmd(fromname, (char *) $3);
249 				free(fromname);
250 				fromname = (char *) 0;
251 			} else {
252 				reply(503, "Bad sequence of commands.");
253 			}
254 			free((char *) $3);
255 		}
256 	|	ABOR CRLF
257 		= {
258 			reply(225, "ABOR command successful.");
259 		}
260 	|	CWD check_login CRLF
261 		= {
262 			if ($2)
263 				cwd(pw->pw_dir);
264 		}
265 	|	CWD check_login SP pathname CRLF
266 		= {
267 			if ($2 && $4 != NULL)
268 				cwd((char *) $4);
269 			if ($4 != NULL)
270 				free((char *) $4);
271 		}
272 	|	HELP CRLF
273 		= {
274 			help((char *) 0);
275 		}
276 	|	HELP SP STRING CRLF
277 		= {
278 			help((char *) $3);
279 		}
280 	|	NOOP CRLF
281 		= {
282 			reply(200, "NOOP command successful.");
283 		}
284 	|	XMKD check_login SP pathname CRLF
285 		= {
286 			if ($2 && $4 != NULL)
287 				makedir((char *) $4);
288 			if ($4 != NULL)
289 				free((char *) $4);
290 		}
291 	|	XRMD check_login SP pathname CRLF
292 		= {
293 			if ($2 && $4 != NULL)
294 				removedir((char *) $4);
295 			if ($4 != NULL)
296 				free((char *) $4);
297 		}
298 	|	XPWD check_login CRLF
299 		= {
300 			if ($2)
301 				pwd();
302 		}
303 	|	XCUP check_login CRLF
304 		= {
305 			if ($2)
306 				cwd("..");
307 		}
308 	|	STOU check_login SP pathname CRLF
309 		= {
310 			if ($2 && $4 != NULL) {
311 				unique++;
312 				store((char *) $4, "w");
313 				unique = 0;
314 			}
315 			if ($4 != NULL)
316 				free((char *) $4);
317 		}
318 	|	QUIT CRLF
319 		= {
320 			reply(221, "Goodbye.");
321 			dologout(0);
322 		}
323 	|	error CRLF
324 		= {
325 			yyerrok;
326 		}
327 	;
328 
329 rcmd:		RNFR check_login SP pathname CRLF
330 		= {
331 			char *renamefrom();
332 
333 			if ($2 && $4) {
334 				fromname = renamefrom((char *) $4);
335 				if (fromname == (char *) 0 && $4) {
336 					free((char *) $4);
337 				}
338 			}
339 		}
340 	;
341 
342 username:	STRING
343 	;
344 
345 password:	STRING
346 	;
347 
348 byte_size:	NUMBER
349 	;
350 
351 host_port:	NUMBER COMMA NUMBER COMMA NUMBER COMMA NUMBER COMMA
352 		NUMBER COMMA NUMBER
353 		= {
354 			register char *a, *p;
355 
356 			a = (char *)&data_dest.sin_addr;
357 			a[0] = $1; a[1] = $3; a[2] = $5; a[3] = $7;
358 			p = (char *)&data_dest.sin_port;
359 			p[0] = $9; p[1] = $11;
360 			data_dest.sin_family = AF_INET;
361 		}
362 	;
363 
364 form_code:	N
365 	= {
366 		$$ = FORM_N;
367 	}
368 	|	T
369 	= {
370 		$$ = FORM_T;
371 	}
372 	|	C
373 	= {
374 		$$ = FORM_C;
375 	}
376 	;
377 
378 type_code:	A
379 	= {
380 		cmd_type = TYPE_A;
381 		cmd_form = FORM_N;
382 	}
383 	|	A SP form_code
384 	= {
385 		cmd_type = TYPE_A;
386 		cmd_form = $3;
387 	}
388 	|	E
389 	= {
390 		cmd_type = TYPE_E;
391 		cmd_form = FORM_N;
392 	}
393 	|	E SP form_code
394 	= {
395 		cmd_type = TYPE_E;
396 		cmd_form = $3;
397 	}
398 	|	I
399 	= {
400 		cmd_type = TYPE_I;
401 	}
402 	|	L
403 	= {
404 		cmd_type = TYPE_L;
405 		cmd_bytesz = 8;
406 	}
407 	|	L SP byte_size
408 	= {
409 		cmd_type = TYPE_L;
410 		cmd_bytesz = $3;
411 	}
412 	/* this is for a bug in the BBN ftp */
413 	|	L byte_size
414 	= {
415 		cmd_type = TYPE_L;
416 		cmd_bytesz = $2;
417 	}
418 	;
419 
420 struct_code:	F
421 	= {
422 		$$ = STRU_F;
423 	}
424 	|	R
425 	= {
426 		$$ = STRU_R;
427 	}
428 	|	P
429 	= {
430 		$$ = STRU_P;
431 	}
432 	;
433 
434 mode_code:	S
435 	= {
436 		$$ = MODE_S;
437 	}
438 	|	B
439 	= {
440 		$$ = MODE_B;
441 	}
442 	|	C
443 	= {
444 		$$ = MODE_C;
445 	}
446 	;
447 
448 pathname:	pathstring
449 	= {
450 		/*
451 		 * Problem: this production is used for all pathname
452 		 * processing, but only gives a 550 error reply.
453 		 * This is a valid reply in some cases but not in others.
454 		 */
455 		if ($1 && strncmp((char *) $1, "~", 1) == 0) {
456 			$$ = (int)*glob((char *) $1);
457 			if (globerr != NULL) {
458 				reply(550, globerr);
459 				$$ = NULL;
460 			}
461 			free((char *) $1);
462 		} else
463 			$$ = $1;
464 	}
465 	;
466 
467 pathstring:	STRING
468 	;
469 
470 check_login:	/* empty */
471 	= {
472 		if (logged_in)
473 			$$ = 1;
474 		else {
475 			reply(530, "Please login with USER and PASS.");
476 			$$ = 0;
477 		}
478 	}
479 	;
480 
481 %%
482 
483 extern jmp_buf errcatch;
484 
485 #define	CMD	0	/* beginning of command */
486 #define	ARGS	1	/* expect miscellaneous arguments */
487 #define	STR1	2	/* expect SP followed by STRING */
488 #define	STR2	3	/* expect STRING */
489 #define	OSTR	4	/* optional STRING */
490 
491 struct tab {
492 	char	*name;
493 	short	token;
494 	short	state;
495 	short	implemented;	/* 1 if command is implemented */
496 	char	*help;
497 };
498 
499 struct tab cmdtab[] = {		/* In order defined in RFC 765 */
500 	{ "USER", USER, STR1, 1,	"<sp> username" },
501 	{ "PASS", PASS, STR1, 1,	"<sp> password" },
502 	{ "ACCT", ACCT, STR1, 0,	"(specify account)" },
503 	{ "REIN", REIN, ARGS, 0,	"(reinitialize server state)" },
504 	{ "QUIT", QUIT, ARGS, 1,	"(terminate service)", },
505 	{ "PORT", PORT, ARGS, 1,	"<sp> b0, b1, b2, b3, b4" },
506 	{ "PASV", PASV, ARGS, 1,	"(set server in passive mode)" },
507 	{ "TYPE", TYPE, ARGS, 1,	"<sp> [ A | E | I | L ]" },
508 	{ "STRU", STRU, ARGS, 1,	"(specify file structure)" },
509 	{ "MODE", MODE, ARGS, 1,	"(specify transfer mode)" },
510 	{ "RETR", RETR, STR1, 1,	"<sp> file-name" },
511 	{ "STOR", STOR, STR1, 1,	"<sp> file-name" },
512 	{ "APPE", APPE, STR1, 1,	"<sp> file-name" },
513 	{ "MLFL", MLFL, OSTR, 0,	"(mail file)" },
514 	{ "MAIL", MAIL, OSTR, 0,	"(mail to user)" },
515 	{ "MSND", MSND, OSTR, 0,	"(mail send to terminal)" },
516 	{ "MSOM", MSOM, OSTR, 0,	"(mail send to terminal or mailbox)" },
517 	{ "MSAM", MSAM, OSTR, 0,	"(mail send to terminal and mailbox)" },
518 	{ "MRSQ", MRSQ, OSTR, 0,	"(mail recipient scheme question)" },
519 	{ "MRCP", MRCP, STR1, 0,	"(mail recipient)" },
520 	{ "ALLO", ALLO, ARGS, 1,	"allocate storage (vacuously)" },
521 	{ "REST", REST, STR1, 0,	"(restart command)" },
522 	{ "RNFR", RNFR, STR1, 1,	"<sp> file-name" },
523 	{ "RNTO", RNTO, STR1, 1,	"<sp> file-name" },
524 	{ "ABOR", ABOR, ARGS, 1,	"(abort operation)" },
525 	{ "DELE", DELE, STR1, 1,	"<sp> file-name" },
526 	{ "CWD",  CWD,  OSTR, 1,	"[ <sp> directory-name]" },
527 	{ "XCWD", CWD,	OSTR, 1,	"[ <sp> directory-name ]" },
528 	{ "LIST", LIST, OSTR, 1,	"[ <sp> path-name ]" },
529 	{ "NLST", NLST, OSTR, 1,	"[ <sp> path-name ]" },
530 	{ "SITE", SITE, STR1, 0,	"(get site parameters)" },
531 	{ "STAT", STAT, OSTR, 0,	"(get server status)" },
532 	{ "HELP", HELP, OSTR, 1,	"[ <sp> <string> ]" },
533 	{ "NOOP", NOOP, ARGS, 1,	"" },
534 	{ "MKD",  XMKD, STR1, 1,	"<sp> path-name" },
535 	{ "XMKD", XMKD, STR1, 1,	"<sp> path-name" },
536 	{ "RMD",  XRMD, STR1, 1,	"<sp> path-name" },
537 	{ "XRMD", XRMD, STR1, 1,	"<sp> path-name" },
538 	{ "PWD",  XPWD, ARGS, 1,	"(return current directory)" },
539 	{ "XPWD", XPWD, ARGS, 1,	"(return current directory)" },
540 	{ "CDUP", XCUP, ARGS, 1,	"(change to parent directory)" },
541 	{ "XCUP", XCUP, ARGS, 1,	"(change to parent directory)" },
542 	{ "STOU", STOU, STR1, 1,	"<sp> file-name" },
543 	{ NULL,   0,    0,    0,	0 }
544 };
545 
546 struct tab *
547 lookup(cmd)
548 	char *cmd;
549 {
550 	register struct tab *p;
551 
552 	for (p = cmdtab; p->name != NULL; p++)
553 		if (strcmp(cmd, p->name) == 0)
554 			return (p);
555 	return (0);
556 }
557 
558 #include <arpa/telnet.h>
559 
560 /*
561  * getline - a hacked up version of fgets to ignore TELNET escape codes.
562  */
563 char *
564 getline(s, n, iop)
565 	char *s;
566 	register FILE *iop;
567 {
568 	register c;
569 	register char *cs;
570 
571 	cs = s;
572 /* tmpline may contain saved command from urgent mode interruption */
573 	for (c = 0; tmpline[c] != '\0' && --n > 0; ++c) {
574 		*cs++ = tmpline[c];
575 		if (tmpline[c] == '\n') {
576 			*cs++ = '\0';
577 			if (debug) {
578 				syslog(LOG_DEBUG, "FTPD: command: %s", s);
579 			}
580 			tmpline[0] = '\0';
581 			return(s);
582 		}
583 		if (c == 0) {
584 			tmpline[0] = '\0';
585 		}
586 	}
587 	while (--n > 0 && (c = getc(iop)) != EOF) {
588 		c = 0377 & c;
589 		while (c == IAC) {
590 			switch (c = 0377 & getc(iop)) {
591 			case WILL:
592 			case WONT:
593 				c = 0377 & getc(iop);
594 				printf("%c%c%c", IAC, WONT, c);
595 				(void) fflush(stdout);
596 				break;
597 			case DO:
598 			case DONT:
599 				c = 0377 & getc(iop);
600 				printf("%c%c%c", IAC, DONT, c);
601 				(void) fflush(stdout);
602 				break;
603 			default:
604 				break;
605 			}
606 			c = 0377 & getc(iop); /* try next character */
607 		}
608 		*cs++ = c;
609 		if (c=='\n')
610 			break;
611 	}
612 	if (c == EOF && cs == s)
613 		return (NULL);
614 	*cs++ = '\0';
615 	if (debug) {
616 		syslog(LOG_DEBUG, "FTPD: command: %s", s);
617 	}
618 	return (s);
619 }
620 
621 static int
622 toolong()
623 {
624 	time_t now;
625 	extern char *ctime();
626 	extern time_t time();
627 
628 	reply(421,
629 	  "Timeout (%d seconds): closing control connection.", timeout);
630 	(void) time(&now);
631 	if (logging) {
632 		syslog(LOG_INFO,
633 			"FTPD: User %s timed out after %d seconds at %s",
634 			(pw ? pw -> pw_name : "unknown"), timeout, ctime(&now));
635 	}
636 	dologout(1);
637 }
638 
639 yylex()
640 {
641 	static int cpos, state;
642 	register char *cp;
643 	register struct tab *p;
644 	int n;
645 	char c;
646 
647 	for (;;) {
648 		switch (state) {
649 
650 		case CMD:
651 			(void) signal(SIGALRM, toolong);
652 			(void) alarm((unsigned) timeout);
653 			if (getline(cbuf, sizeof(cbuf)-1, stdin) == NULL) {
654 				reply(221, "You could at least say goodbye.");
655 				dologout(0);
656 			}
657 			(void) alarm(0);
658 			if (index(cbuf, '\r')) {
659 				cp = index(cbuf, '\r');
660 				cp[0] = '\n'; cp[1] = 0;
661 			}
662 			if (index(cbuf, ' '))
663 				cpos = index(cbuf, ' ') - cbuf;
664 			else
665 				cpos = index(cbuf, '\n') - cbuf;
666 			if (cpos == 0) {
667 				cpos = 4;
668 			}
669 			c = cbuf[cpos];
670 			cbuf[cpos] = '\0';
671 			upper(cbuf);
672 			p = lookup(cbuf);
673 			cbuf[cpos] = c;
674 			if (p != 0) {
675 				if (p->implemented == 0) {
676 					nack(p->name);
677 					longjmp(errcatch,0);
678 					/* NOTREACHED */
679 				}
680 				state = p->state;
681 				yylval = (int) p->name;
682 				return (p->token);
683 			}
684 			break;
685 
686 		case OSTR:
687 			if (cbuf[cpos] == '\n') {
688 				state = CMD;
689 				return (CRLF);
690 			}
691 			/* FALL THRU */
692 
693 		case STR1:
694 			if (cbuf[cpos] == ' ') {
695 				cpos++;
696 				state = STR2;
697 				return (SP);
698 			}
699 			break;
700 
701 		case STR2:
702 			cp = &cbuf[cpos];
703 			n = strlen(cp);
704 			cpos += n - 1;
705 			/*
706 			 * Make sure the string is nonempty and \n terminated.
707 			 */
708 			if (n > 1 && cbuf[cpos] == '\n') {
709 				cbuf[cpos] = '\0';
710 				yylval = copy(cp);
711 				cbuf[cpos] = '\n';
712 				state = ARGS;
713 				return (STRING);
714 			}
715 			break;
716 
717 		case ARGS:
718 			if (isdigit(cbuf[cpos])) {
719 				cp = &cbuf[cpos];
720 				while (isdigit(cbuf[++cpos]))
721 					;
722 				c = cbuf[cpos];
723 				cbuf[cpos] = '\0';
724 				yylval = atoi(cp);
725 				cbuf[cpos] = c;
726 				return (NUMBER);
727 			}
728 			switch (cbuf[cpos++]) {
729 
730 			case '\n':
731 				state = CMD;
732 				return (CRLF);
733 
734 			case ' ':
735 				return (SP);
736 
737 			case ',':
738 				return (COMMA);
739 
740 			case 'A':
741 			case 'a':
742 				return (A);
743 
744 			case 'B':
745 			case 'b':
746 				return (B);
747 
748 			case 'C':
749 			case 'c':
750 				return (C);
751 
752 			case 'E':
753 			case 'e':
754 				return (E);
755 
756 			case 'F':
757 			case 'f':
758 				return (F);
759 
760 			case 'I':
761 			case 'i':
762 				return (I);
763 
764 			case 'L':
765 			case 'l':
766 				return (L);
767 
768 			case 'N':
769 			case 'n':
770 				return (N);
771 
772 			case 'P':
773 			case 'p':
774 				return (P);
775 
776 			case 'R':
777 			case 'r':
778 				return (R);
779 
780 			case 'S':
781 			case 's':
782 				return (S);
783 
784 			case 'T':
785 			case 't':
786 				return (T);
787 
788 			}
789 			break;
790 
791 		default:
792 			fatal("Unknown state in scanner.");
793 		}
794 		yyerror((char *) 0);
795 		state = CMD;
796 		longjmp(errcatch,0);
797 	}
798 }
799 
800 upper(s)
801 	char *s;
802 {
803 	while (*s != '\0') {
804 		if (islower(*s))
805 			*s = toupper(*s);
806 		s++;
807 	}
808 }
809 
810 copy(s)
811 	char *s;
812 {
813 	char *p;
814 	extern char *malloc(), *strcpy();
815 
816 	p = malloc((unsigned) strlen(s) + 1);
817 	if (p == NULL)
818 		fatal("Ran out of memory.");
819 	(void) strcpy(p, s);
820 	return ((int)p);
821 }
822 
823 help(s)
824 	char *s;
825 {
826 	register struct tab *c;
827 	register int width, NCMDS;
828 
829 	width = 0, NCMDS = 0;
830 	for (c = cmdtab; c->name != NULL; c++) {
831 		int len = strlen(c->name) + 1;
832 
833 		if (len > width)
834 			width = len;
835 		NCMDS++;
836 	}
837 	width = (width + 8) &~ 7;
838 	if (s == 0) {
839 		register int i, j, w;
840 		int columns, lines;
841 
842 		lreply(214,
843 	  "The following commands are recognized (* =>'s unimplemented).");
844 		columns = 76 / width;
845 		if (columns == 0)
846 			columns = 1;
847 		lines = (NCMDS + columns - 1) / columns;
848 		for (i = 0; i < lines; i++) {
849 			printf("   ");
850 			for (j = 0; j < columns; j++) {
851 				c = cmdtab + j * lines + i;
852 				printf("%s%c", c->name,
853 					c->implemented ? ' ' : '*');
854 				if (c + lines >= &cmdtab[NCMDS])
855 					break;
856 				w = strlen(c->name) + 1;
857 				while (w < width) {
858 					putchar(' ');
859 					w++;
860 				}
861 			}
862 			printf("\r\n");
863 		}
864 		(void) fflush(stdout);
865 		reply(214, "Direct comments to ftp-bugs@%s.", hostname);
866 		return;
867 	}
868 	upper(s);
869 	c = lookup(s);
870 	if (c == (struct tab *)0) {
871 		reply(502, "Unknown command %s.", s);
872 		return;
873 	}
874 	if (c->implemented)
875 		reply(214, "Syntax: %s %s", c->name, c->help);
876 	else
877 		reply(214, "%-*s\t%s; unimplemented.", width, c->name, c->help);
878 }
879