xref: /titanic_44/usr/src/cmd/cmd-inet/usr.bin/rdist/docmd.c (revision 843e19887f64dde75055cf8842fc4db2171eff45)
1 /*
2  * Copyright 2005 Sun Microsystems, Inc.  All rights reserved.
3  * Use is subject to license terms.
4  */
5 
6 /*
7  * Copyright (c) 1983 Regents of the University of California.
8  * All rights reserved.
9  *
10  * Redistribution and use in source and binary forms are permitted
11  * provided that the above copyright notice and this paragraph are
12  * duplicated in all such forms and that any documentation,
13  * advertising materials, and other materials related to such
14  * distribution and use acknowledge that the software was developed
15  * by the University of California, Berkeley.  The name of the
16  * University may not be used to endorse or promote products derived
17  * from this software without specific prior written permission.
18  */
19 #pragma ident	"%Z%%M%	%I%	%E% SMI"
20 
21 #include "defs.h"
22 #include <string.h>
23 #include <setjmp.h>
24 #include <netdb.h>
25 #include <signal.h>
26 #include <krb5defs.h>
27 
28 #ifndef RDIST
29 #ifdef SYSV
30 /*
31  * Historically, the rdist program has had the following hard-coded
32  * pathname.  Some operating systems attempt to "improve" the
33  * directory layout, in the process re-locating the rdist binary
34  * to some other location.  However, the first original implementation
35  * sets a standard of sorts.  In order to interoperate with other
36  * systems, our implementation must do two things: It must provide
37  * the an rdist binary at the pathname below, and it must use this
38  * pathname when executing rdist on remote systems via the rcmd()
39  * library.  Thus the hard-coded path name below can never be changed.
40  */
41 #endif /* SYSV */
42 #define	RDIST "/usr/ucb/rdist"
43 #endif
44 
45 FILE	*lfp;			/* log file for recording files updated */
46 struct	subcmd *subcmds;	/* list of sub-commands for current cmd */
47 jmp_buf	env;
48 
49 void	cleanup();
50 void	lostconn();
51 static int	init_service(int);
52 static struct servent *sp;
53 
54 static void notify(char *file, char *rhost, struct namelist *to, time_t lmod);
55 static void rcmptime(struct stat *st);
56 static void cmptime(char *name);
57 static void dodcolon(char **filev, struct namelist *files, char *stamp,
58     struct subcmd *cmds);
59 static void closeconn(void);
60 static void doarrow(char **filev, struct namelist *files, char *rhost,
61     struct subcmd *cmds);
62 static int makeconn(char *rhost);
63 static int okname(register char *name);
64 
65 #ifdef SYSV
66 #include <libgen.h>
67 
68 static char *recomp;
69 static char *errstring = "regcmp failed for some unknown reason";
70 
71 char *
72 re_comp(s)
73 char *s;
74 {
75 	if ((int)recomp != 0)
76 		free(recomp);
77 	recomp = regcmp(s, (char *)0);
78 	if (recomp == NULL)
79 		return (errstring);
80 	else
81 		return ((char *)0);
82 }
83 
84 
85 static int
86 re_exec(s)
87 char *s;
88 {
89 	if ((int)recomp == 0)
90 		return (-1);
91 	if (regex(recomp, s) == NULL)
92 		return (0);
93 	else
94 		return (1);
95 }
96 #endif /* SYSV */
97 
98 /*
99  * Do the commands in cmds (initialized by yyparse).
100  */
101 void
102 docmds(dhosts, argc, argv)
103 	char **dhosts;
104 	int argc;
105 	char **argv;
106 {
107 	register struct cmd *c;
108 	register struct namelist *f;
109 	register char **cpp;
110 	extern struct cmd *cmds;
111 
112 	/* protect backgrounded rdist */
113 	if (signal(SIGINT, SIG_IGN) != SIG_IGN)
114 		(void) signal(SIGINT, cleanup);
115 
116 	/* ... and running via nohup(1) */
117 	if (signal(SIGHUP, SIG_IGN) != SIG_IGN)
118 		(void) signal(SIGHUP, cleanup);
119 	if (signal(SIGQUIT, SIG_IGN) != SIG_IGN)
120 		(void) signal(SIGQUIT, cleanup);
121 
122 	(void) signal(SIGTERM, cleanup);
123 
124 if (debug)
125 	if (!cmds)
126 		printf("docmds:  cmds == NULL\n");
127 	else {
128 		printf("docmds:  cmds ");
129 		prcmd(cmds);
130 	}
131 	for (c = cmds; c != NULL; c = c->c_next) {
132 		if (dhosts != NULL && *dhosts != NULL) {
133 			for (cpp = dhosts; *cpp; cpp++)
134 				if (strcmp(c->c_name, *cpp) == 0)
135 					goto fndhost;
136 			continue;
137 		}
138 	fndhost:
139 		if (argc) {
140 			for (cpp = argv; *cpp; cpp++) {
141 				if (c->c_label != NULL &&
142 				    strcmp(c->c_label, *cpp) == 0) {
143 					cpp = NULL;
144 					goto found;
145 				}
146 				for (f = c->c_files; f != NULL; f = f->n_next)
147 					if (strcmp(f->n_name, *cpp) == 0)
148 						goto found;
149 			}
150 			continue;
151 		} else
152 			cpp = NULL;
153 	found:
154 		switch (c->c_type) {
155 		case ARROW:
156 			doarrow(cpp, c->c_files, c->c_name, c->c_cmds);
157 			break;
158 		case DCOLON:
159 			dodcolon(cpp, c->c_files, c->c_name, c->c_cmds);
160 			break;
161 		default:
162 			fatal("illegal command type %d\n", c->c_type);
163 		}
164 	}
165 	closeconn();
166 }
167 
168 /*
169  * Process commands for sending files to other machines.
170  */
171 static void
172 doarrow(filev, files, rhost, cmds)
173 	char **filev;
174 	struct namelist *files;
175 	char *rhost;
176 	struct subcmd *cmds;
177 {
178 	register struct namelist *f;
179 	register struct subcmd *sc;
180 	register char **cpp;
181 	int n, ddir, opts = options;
182 
183 	if (debug)
184 		printf("doarrow(%x, %s, %x)\n", files, rhost, cmds);
185 
186 	if (files == NULL) {
187 		error("no files to be updated\n");
188 		return;
189 	}
190 
191 	subcmds = cmds;
192 	ddir = files->n_next != NULL;	/* destination is a directory */
193 	if (nflag)
194 		printf("updating host %s\n", rhost);
195 	else {
196 		if (setjmp(env))
197 			goto done;
198 		(void) signal(SIGPIPE, lostconn);
199 		if (!makeconn(rhost))
200 			return;
201 		if (!nflag)
202 			if ((lfp = fopen(Tmpfile, "w")) == NULL) {
203 				fatal("cannot open %s\n", Tmpfile);
204 				exit(1);
205 			}
206 	}
207 	for (f = files; f != NULL; f = f->n_next) {
208 		if (filev) {
209 			for (cpp = filev; *cpp; cpp++)
210 				if (strcmp(f->n_name, *cpp) == 0)
211 					goto found;
212 			continue;
213 		}
214 	found:
215 		n = 0;
216 		for (sc = cmds; sc != NULL; sc = sc->sc_next) {
217 			if (sc->sc_type != INSTALL)
218 				continue;
219 			n++;
220 			install(f->n_name, sc->sc_name,
221 				sc->sc_name == NULL ? 0 : ddir, sc->sc_options);
222 			opts = sc->sc_options;
223 		}
224 		if (n == 0)
225 			install(f->n_name, NULL, 0, options);
226 	}
227 done:
228 	if (!nflag) {
229 		(void) signal(SIGPIPE, cleanup);
230 		(void) fclose(lfp);
231 		lfp = NULL;
232 	}
233 	for (sc = cmds; sc != NULL; sc = sc->sc_next)
234 		if (sc->sc_type == NOTIFY)
235 			notify(Tmpfile, rhost, sc->sc_args, 0);
236 	if (!nflag) {
237 		(void) unlink(Tmpfile);
238 		for (; ihead != NULL; ihead = ihead->nextp) {
239 			free(ihead);
240 			if ((opts & IGNLNKS) || ihead->count == 0)
241 				continue;
242 			log(lfp, "%s: Warning: missing links\n",
243 				ihead->pathname);
244 		}
245 	}
246 }
247 
248 static int
249 init_service(int krb5flag)
250 {
251 	boolean_t success = B_FALSE;
252 
253 	if (krb5flag > 0) {
254 		if ((sp = getservbyname("kshell", "tcp")) == NULL) {
255 			fatal("kshell/tcp: unknown service");
256 			(void) fprintf(stderr,
257 				gettext("trying shell/tcp service...\n"));
258 		} else {
259 			success = B_TRUE;
260 		}
261 	} else {
262 		if ((sp = getservbyname("shell", "tcp")) == NULL) {
263 			fatal("shell/tcp: unknown service");
264 			exit(1);
265 		} else {
266 			success = B_TRUE;
267 		}
268 	}
269 	return (success);
270 }
271 /*
272  * Create a connection to the rdist server on the machine rhost.
273  */
274 static int
275 makeconn(rhost)
276 	char *rhost;
277 {
278 	register char *ruser, *cp;
279 	static char *cur_host = NULL;
280 	static int port = -1;
281 	char tuser[20];
282 	int n;
283 	extern char user[];
284 
285 	if (debug)
286 		printf("makeconn(%s)\n", rhost);
287 
288 	if (cur_host != NULL && rem >= 0) {
289 		if (strcmp(cur_host, rhost) == 0)
290 			return (1);
291 		closeconn();
292 	}
293 	cur_host = rhost;
294 	cp = index(rhost, '@');
295 	if (cp != NULL) {
296 		char c = *cp;
297 
298 		*cp = '\0';
299 		strncpy(tuser, rhost, sizeof (tuser)-1);
300 		*cp = c;
301 		rhost = cp + 1;
302 		ruser = tuser;
303 		if (*ruser == '\0')
304 			ruser = user;
305 		else if (!okname(ruser))
306 			return (0);
307 	} else
308 		ruser = user;
309 	if (!qflag)
310 		printf("updating host %s\n", rhost);
311 	(void) snprintf(buf, RDIST_BUFSIZ, "%s%s -Server%s",
312 			encrypt_flag ? "-x " : "", RDIST, qflag ? " -q" : "");
313 	if (port < 0) {
314 		if (debug_port == 0) {
315 			if ((retval = (int)init_service(krb5auth_flag)) == 0) {
316 				krb5auth_flag = encrypt_flag = 0;
317 				(void) init_service(krb5auth_flag);
318 			}
319 			port = sp->s_port;
320 
321 		} else {
322 			port = debug_port;
323 		}
324 	}
325 
326 	if (debug) {
327 		printf("port = %d, luser = %s, ruser = %s\n", ntohs(port),
328 			user, ruser);
329 		printf("buf = %s\n", buf);
330 	}
331 
332 	fflush(stdout);
333 
334 	if (krb5auth_flag > 0) {
335 		if ((encrypt_flag > 0) && (!krb5_privacy_allowed())) {
336 			(void) fprintf(stderr, gettext("rdist: Encryption "
337 					" not supported.\n"));
338 			exit(1);
339 		}
340 
341 		authopts = AP_OPTS_MUTUAL_REQUIRED;
342 
343 		status = kcmd(&rem, &rhost, port,
344 				user, ruser,
345 				buf, 0, "host", krb_realm,
346 				bsd_context,
347 				&auth_context,
348 				&cred,
349 				0,	/* No need for sequence number */
350 				0,	/* No need for server seq # */
351 				authopts,
352 				1,	/* Always set anyport */
353 				&kcmd_proto);
354 		if (status) {
355 			/*
356 			 * If new protocol requested, we dont
357 			 * fallback to less secure ones.
358 			 */
359 			if (kcmd_proto == KCMD_NEW_PROTOCOL) {
360 				(void) fprintf(stderr, gettext("rdist: kcmdv2 "
361 					"to host %s failed - %s\n"
362 					"Fallback to normal rdist denied."),
363 					host, error_message(status));
364 				exit(1);
365 			}
366 			/* check NO_TKT_FILE or equivalent... */
367 			if (status != -1) {
368 				(void) fprintf(stderr, gettext("rdist: "
369 				"kcmd to host %s failed - %s\n"
370 				"trying normal rdist...\n\n"),
371 				host, error_message(status));
372 			} else {
373 				(void) fprintf(stderr,
374 					gettext("trying normal rdist...\n"));
375 			}
376 			/*
377 			 * kcmd() failed, so we now fallback to normal rdist
378 			 */
379 			krb5auth_flag = encrypt_flag = 0;
380 			(void) init_service(krb5auth_flag);
381 			port = sp->s_port;
382 			goto do_rcmd;
383 		}
384 #ifdef DEBUG
385 		else {
386 			(void) fprintf(stderr, gettext("Kerberized rdist "
387 					"session, port %d in use "), port);
388 			if (kcmd_proto == KCMD_OLD_PROTOCOL)
389 				(void) fprintf(stderr,
390 						gettext("[kcmd ver.1].\n"));
391 			else
392 				(void) fprintf(stderr,
393 						gettext("[kcmd ver.2].\n"));
394 		}
395 #endif /* DEBUG */
396 		session_key = &cred->keyblock;
397 
398 		if (kcmd_proto == KCMD_NEW_PROTOCOL) {
399 			status = krb5_auth_con_getlocalsubkey(bsd_context,
400 							    auth_context,
401 							    &session_key);
402 			if (status) {
403 				com_err("rdist", status,
404 					"determining subkey for session");
405 				exit(1);
406 			}
407 			if (!session_key) {
408 				com_err("rdist", 0,
409 				"no subkey negotiated for connection");
410 				exit(1);
411 			}
412 		}
413 
414 		eblock.crypto_entry = session_key->enctype;
415 		eblock.key = (krb5_keyblock *)session_key;
416 
417 		init_encrypt(encrypt_flag, bsd_context, kcmd_proto, &desinbuf,
418 				&desoutbuf, CLIENT, &eblock);
419 
420 
421 		if (encrypt_flag > 0) {
422 			char *s = gettext("This rdist session is using "
423 				"encryption for all data transmissions.\r\n");
424 			(void) write(2, s, strlen(s));
425 		}
426 
427 	}
428 	else
429 do_rcmd:
430 	{
431 		rem = rcmd_af(&rhost, port, user, ruser, buf, 0, AF_INET6);
432 	}
433 
434 	if (rem < 0)
435 		return (0);
436 
437 	cp = buf;
438 	if (desread(rem, cp, 1, 0) != 1)
439 		lostconn();
440 	if (*cp == 'V') {
441 		do {
442 			if (desread(rem, cp, 1, 0) != 1)
443 				lostconn();
444 		} while (*cp++ != '\n' && cp < &buf[RDIST_BUFSIZ]);
445 		*--cp = '\0';
446 		cp = buf;
447 		n = 0;
448 		while (*cp >= '0' && *cp <= '9')
449 			n = (n * 10) + (*cp++ - '0');
450 		if (*cp == '\0' && n == VERSION)
451 			return (1);
452 		error("connection failed: version numbers don't match"
453 		    " (local %d, remote %d)\n", VERSION, n);
454 	} else {
455 		error("connection failed: version numbers don't match\n");
456 	}
457 	closeconn();
458 	return (0);
459 }
460 
461 /*
462  * Signal end of previous connection.
463  */
464 static void
465 closeconn(void)
466 {
467 	if (debug)
468 		printf("closeconn()\n");
469 
470 	if (rem >= 0) {
471 		(void) deswrite(rem, "\2\n", 2, 0);
472 		(void) close(rem);
473 		rem = -1;
474 	}
475 }
476 
477 void
478 lostconn()
479 {
480 	if (iamremote)
481 		cleanup();
482 	log(lfp, "rdist: lost connection\n");
483 	longjmp(env, 1);
484 }
485 
486 static int
487 okname(name)
488 	register char *name;
489 {
490 	register char *cp = name;
491 	register int c;
492 
493 	do {
494 		c = *cp;
495 		if (c & 0200)
496 			goto bad;
497 		if (!isalpha(c) && !isdigit(c) && c != '_' && c != '-')
498 			goto bad;
499 		cp++;
500 	} while (*cp);
501 	return (1);
502 bad:
503 	error("invalid user name %s\n", name);
504 	return (0);
505 }
506 
507 time_t	lastmod;
508 FILE	*tfp;
509 extern	char target[], *tp;
510 
511 /*
512  * Process commands for comparing files to time stamp files.
513  */
514 static void
515 dodcolon(filev, files, stamp, cmds)
516 	char **filev;
517 	struct namelist *files;
518 	char *stamp;
519 	struct subcmd *cmds;
520 {
521 	register struct subcmd *sc;
522 	register struct namelist *f;
523 	register char **cpp;
524 	struct timeval tv[2];
525 	struct stat stb;
526 
527 	if (debug)
528 		printf("dodcolon()\n");
529 
530 	if (files == NULL) {
531 		error("no files to be updated\n");
532 		return;
533 	}
534 	if (stat(stamp, &stb) < 0) {
535 		error("%s: %s\n", stamp, strerror(errno));
536 		return;
537 	}
538 	if (debug)
539 		printf("%s: %d\n", stamp, stb.st_mtime);
540 
541 	subcmds = cmds;
542 	lastmod = stb.st_mtime;
543 	if (nflag || (options & VERIFY))
544 		tfp = NULL;
545 	else {
546 		if ((tfp = fopen(Tmpfile, "w")) == NULL) {
547 			error("%s: %s\n", stamp, strerror(errno));
548 			return;
549 		}
550 		(void) gettimeofday(&tv[0], (struct timezone *)NULL);
551 		tv[1] = tv[0];
552 		(void) utimes(stamp, tv);
553 	}
554 
555 	for (f = files; f != NULL; f = f->n_next) {
556 		if (filev) {
557 			for (cpp = filev; *cpp; cpp++)
558 				if (strcmp(f->n_name, *cpp) == 0)
559 					goto found;
560 			continue;
561 		}
562 	found:
563 		tp = NULL;
564 		cmptime(f->n_name);
565 	}
566 
567 	if (tfp != NULL)
568 		(void) fclose(tfp);
569 	for (sc = cmds; sc != NULL; sc = sc->sc_next)
570 		if (sc->sc_type == NOTIFY)
571 			notify(Tmpfile, NULL, sc->sc_args, lastmod);
572 	if (!nflag && !(options & VERIFY))
573 		(void) unlink(Tmpfile);
574 }
575 
576 /*
577  * Compare the mtime of file to the list of time stamps.
578  */
579 static void
580 cmptime(name)
581 	char *name;
582 {
583 	struct stat stb;
584 
585 	if (debug)
586 		printf("cmptime(%s)\n", name);
587 
588 	if (except(name))
589 		return;
590 
591 	if (nflag) {
592 		printf("comparing dates: %s\n", name);
593 		return;
594 	}
595 
596 	/*
597 	 * first time cmptime() is called?
598 	 */
599 	if (tp == NULL) {
600 		if (exptilde(target, RDIST_BUFSIZ, name) == NULL)
601 			return;
602 		tp = name = target;
603 		while (*tp)
604 			tp++;
605 	}
606 	if (access(name, 4) < 0 || stat(name, &stb) < 0) {
607 		error("%s: %s\n", name, strerror(errno));
608 		return;
609 	}
610 
611 	switch (stb.st_mode & S_IFMT) {
612 	case S_IFREG:
613 		break;
614 
615 	case S_IFDIR:
616 		rcmptime(&stb);
617 		return;
618 
619 	default:
620 		error("%s: not a plain file\n", name);
621 		return;
622 	}
623 
624 	if (stb.st_mtime > lastmod)
625 		log(tfp, "new: %s\n", name);
626 }
627 
628 static void
629 rcmptime(st)
630 	struct stat *st;
631 {
632 	register DIR *d;
633 	register struct dirent *dp;
634 	register char *cp;
635 	char *otp;
636 	int len;
637 
638 	if (debug)
639 		printf("rcmptime(%x)\n", st);
640 
641 	if ((d = opendir(target)) == NULL) {
642 		error("%s: %s\n", target, strerror(errno));
643 		return;
644 	}
645 	otp = tp;
646 	len = tp - target;
647 	while (dp = readdir(d)) {
648 		if ((strcmp(dp->d_name, ".") == 0) ||
649 		    (strcmp(dp->d_name, "..") == 0))
650 			continue;
651 		if (len + 1 + strlen(dp->d_name) >= RDIST_BUFSIZ - 1) {
652 			error("%s/%s: Name too long\n", target, dp->d_name);
653 			continue;
654 		}
655 		tp = otp;
656 		*tp++ = '/';
657 		cp = dp->d_name;
658 		while (*tp++ = *cp++)
659 			;
660 		tp--;
661 		cmptime(target);
662 	}
663 	closedir(d);
664 	tp = otp;
665 	*tp = '\0';
666 }
667 
668 /*
669  * Notify the list of people the changes that were made.
670  * rhost == NULL if we are mailing a list of changes compared to at time
671  * stamp file.
672  */
673 static void
674 notify(file, rhost, to, lmod)
675 	char *file, *rhost;
676 	register struct namelist *to;
677 	time_t lmod;
678 {
679 	register int fd, len;
680 	FILE *pf, *popen();
681 	struct stat stb;
682 
683 	if ((options & VERIFY) || to == NULL)
684 		return;
685 	if (!qflag) {
686 		printf("notify ");
687 		if (rhost)
688 			printf("@%s ", rhost);
689 		prnames(to);
690 	}
691 	if (nflag)
692 		return;
693 
694 	if ((fd = open(file, 0)) < 0) {
695 		error("%s: %s\n", file, strerror(errno));
696 		return;
697 	}
698 	if (fstat(fd, &stb) < 0) {
699 		error("%s: %s\n", file, strerror(errno));
700 		(void) close(fd);
701 		return;
702 	}
703 	if (stb.st_size == 0) {
704 		(void) close(fd);
705 		return;
706 	}
707 	/*
708 	 * Create a pipe to mailling program.
709 	 */
710 	pf = popen(MAILCMD, "w");
711 	if (pf == NULL) {
712 		error("notify: \"%s\" failed\n", MAILCMD);
713 		(void) close(fd);
714 		return;
715 	}
716 	/*
717 	 * Output the proper header information.
718 	 */
719 	fprintf(pf, "From: rdist (Remote distribution program)\n");
720 	fprintf(pf, "To:");
721 	if (!any('@', to->n_name) && rhost != NULL)
722 		fprintf(pf, " %s@%s", to->n_name, rhost);
723 	else
724 		fprintf(pf, " %s", to->n_name);
725 	to = to->n_next;
726 	while (to != NULL) {
727 		if (!any('@', to->n_name) && rhost != NULL)
728 			fprintf(pf, ", %s@%s", to->n_name, rhost);
729 		else
730 			fprintf(pf, ", %s", to->n_name);
731 		to = to->n_next;
732 	}
733 	putc('\n', pf);
734 	if (rhost != NULL)
735 		fprintf(pf, "Subject: files updated by rdist from %s to %s\n",
736 			host, rhost);
737 	else
738 		fprintf(pf, "Subject: files updated after %s\n", ctime(&lmod));
739 	putc('\n', pf);
740 
741 	while ((len = read(fd, buf, RDIST_BUFSIZ)) > 0)
742 		(void) fwrite(buf, 1, len, pf);
743 	(void) close(fd);
744 	(void) pclose(pf);
745 }
746 
747 /*
748  * Return true if name is in the list.
749  */
750 int
751 inlist(list, file)
752 	struct namelist *list;
753 	char *file;
754 {
755 	register struct namelist *nl;
756 
757 	for (nl = list; nl != NULL; nl = nl->n_next)
758 		if (strcmp(file, nl->n_name) == 0)
759 			return (1);
760 	return (0);
761 }
762 
763 /*
764  * Return TRUE if file is in the exception list.
765  */
766 int
767 except(file)
768 	char *file;
769 {
770 	register struct	subcmd *sc;
771 	register struct	namelist *nl;
772 
773 	if (debug)
774 		printf("except(%s)\n", file);
775 
776 	for (sc = subcmds; sc != NULL; sc = sc->sc_next) {
777 		if (sc->sc_type != EXCEPT && sc->sc_type != PATTERN)
778 			continue;
779 		for (nl = sc->sc_args; nl != NULL; nl = nl->n_next) {
780 			if (sc->sc_type == EXCEPT) {
781 				if (strcmp(file, nl->n_name) == 0)
782 					return (1);
783 				continue;
784 			}
785 			re_comp(nl->n_name);
786 			if (re_exec(file) > 0)
787 				return (1);
788 		}
789 	}
790 	return (0);
791 }
792 
793 char *
794 colon(cp)
795 	register char *cp;
796 {
797 
798 	while (*cp) {
799 		if (*cp == ':')
800 			return (cp);
801 		if (*cp == '/')
802 			return (0);
803 		cp++;
804 	}
805 	return (0);
806 }
807