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