xref: /titanic_44/usr/src/lib/libnsl/rpc/netnamer.c (revision fe598cdcd847f8359013532d5c691bb6190378c0)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
9  * or http://www.opensolaris.org/os/licensing.
10  * See the License for the specific language governing permissions
11  * and limitations under the License.
12  *
13  * When distributing Covered Code, include this CDDL HEADER in each
14  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
15  * If applicable, add the following below this CDDL HEADER, with the
16  * fields enclosed by brackets "[]" replaced with your own identifying
17  * information: Portions Copyright [yyyy] [name of copyright owner]
18  *
19  * CDDL HEADER END
20  */
21 
22 /*
23  * Copyright 2007 Sun Microsystems, Inc.  All rights reserved.
24  * Use is subject to license terms.
25  */
26 /* Copyright (c) 1983, 1984, 1985, 1986, 1987, 1988, 1989 AT&T */
27 /* All Rights Reserved */
28 /*
29  * Portions of this source code were derived from Berkeley
30  * 4.3 BSD under license from the Regents of the University of
31  * California.
32  */
33 /*
34  * ==== hack-attack:  possibly MT-safe but definitely not MT-hot.
35  * ==== turn this into a real switch frontend and backends
36  *
37  * Well, at least the API doesn't involve pointers-to-static.
38  */
39 
40 #pragma ident	"%Z%%M%	%I%	%E% SMI"
41 
42 /*
43  * netname utility routines convert from netnames to unix names (uid, gid)
44  *
45  * This module is operating system dependent!
46  * What we define here will work with any unix system that has adopted
47  * the Sun NIS domain architecture.
48  */
49 
50 #undef NIS
51 #include "mt.h"
52 #include "rpc_mt.h"
53 #include <stdio.h>
54 #include <stdlib.h>
55 #include <sys/types.h>
56 #include <ctype.h>
57 #include <grp.h>
58 #include <pwd.h>
59 #include <string.h>
60 #include <syslog.h>
61 #include <sys/param.h>
62 #include <nsswitch.h>
63 #include <rpc/rpc.h>
64 #include <rpcsvc/nis.h>
65 #include <rpcsvc/ypclnt.h>
66 #include <nss_dbdefs.h>
67 
68 static const char    OPSYS[]	= "unix";
69 static const char    NETIDFILE[] = "/etc/netid";
70 static const char    NETID[]	= "netid.byname";
71 static const char    PKTABLE[]  = "cred.org_dir";
72 #define	PKTABLE_LEN 12
73 #define	OPSYS_LEN 4
74 
75 #ifndef NGROUPS
76 #define	NGROUPS 16
77 #endif
78 
79 extern int _getgroupsbymember(const char *, gid_t[], int, int);
80 
81 /*
82  * the value for NOBODY_UID is set by the SVID. The following define also
83  * appears in netname.c
84  */
85 
86 #define	NOBODY_UID 60001
87 
88 /*
89  *	default publickey policy:
90  *		publickey: nis [NOTFOUND = return] files
91  */
92 
93 
94 /*		NSW_NOTSUCCESS  NSW_NOTFOUND   NSW_UNAVAIL    NSW_TRYAGAIN */
95 #define	DEF_ACTION {__NSW_RETURN, __NSW_RETURN, __NSW_CONTINUE, __NSW_CONTINUE}
96 
97 static struct __nsw_lookup lookup_files = {"files", DEF_ACTION, NULL, NULL},
98 		lookup_nis = {"nis", DEF_ACTION, NULL, &lookup_files};
99 static struct __nsw_switchconfig publickey_default =
100 			{0, "publickey", 2, &lookup_nis};
101 
102 static mutex_t serialize_netname_r = DEFAULTMUTEX;
103 
104 struct netid_userdata {
105 	uid_t	*uidp;
106 	gid_t	*gidp;
107 	int	*gidlenp;
108 	gid_t	*gidlist;
109 };
110 
111 static int
112 parse_uid(char *s, struct netid_userdata *argp)
113 {
114 	uid_t	u;
115 
116 	if (!s || !isdigit(*s)) {
117 		syslog(LOG_ERR,
118 			"netname2user: expecting uid '%s'", s);
119 		return (__NSW_NOTFOUND); /* xxx need a better error */
120 	}
121 
122 	/* Fetch the uid */
123 	u = (uid_t)(atoi(s));
124 
125 	if (u == 0) {
126 		syslog(LOG_ERR, "netname2user: should not have uid 0");
127 		return (__NSW_NOTFOUND);
128 	}
129 	*(argp->uidp) = u;
130 	return (__NSW_SUCCESS);
131 }
132 
133 
134 /* parse a comma separated gid list */
135 static int
136 parse_gidlist(char *p, struct netid_userdata *argp)
137 {
138 	int len;
139 	gid_t	g;
140 
141 	if (!p || (!isdigit(*p))) {
142 		syslog(LOG_ERR,
143 			"netname2user: missing group id list in '%s'.",
144 			p);
145 		return (__NSW_NOTFOUND);
146 	}
147 
148 	g = (gid_t)(atoi(p));
149 	*(argp->gidp) = g;
150 
151 	len = 0;
152 	while (p = strchr(p, ','))
153 		argp->gidlist[len++] = (gid_t)atoi(++p);
154 	*(argp->gidlenp) = len;
155 	return (__NSW_SUCCESS);
156 }
157 
158 
159 /*
160  * parse_netid_str()
161  *
162  * Parse uid and group information from the passed string.
163  *
164  * The format of the string passed is
165  * 	uid:gid,grp,grp, ...
166  *
167  */
168 static int
169 parse_netid_str(char *s, struct netid_userdata *argp)
170 {
171 	char	*p;
172 	int	err;
173 
174 	/* get uid */
175 	err = parse_uid(s, argp);
176 	if (err != __NSW_SUCCESS)
177 		return (err);
178 
179 	/* Now get the group list */
180 	p = strchr(s, ':');
181 	if (!p) {
182 		syslog(LOG_ERR,
183 			"netname2user: missing group id list in '%s'", s);
184 		return (__NSW_NOTFOUND);
185 	}
186 	++p;			/* skip ':' */
187 	err = parse_gidlist(p, argp);
188 	return (err);
189 }
190 
191 static int
192 parse_uid_gidlist(char *ustr, char *gstr, struct netid_userdata *argp)
193 {
194 	int	err;
195 
196 	/* get uid */
197 	err = parse_uid(ustr, argp);
198 	if (err != __NSW_SUCCESS)
199 		return (err);
200 
201 	/* Now get the group list */
202 	return (parse_gidlist(gstr, argp));
203 }
204 
205 
206 /*
207  * netname2user_files()
208  *
209  * This routine fetches the netid information from the "files" nameservice.
210  * ie /etc/netid.
211  */
212 static int
213 netname2user_files(int *err, char *netname, struct netid_userdata *argp)
214 {
215 	char 	buf[512];	/* one line from the file */
216 	char	*name;
217 	char	*value;
218 	char 	*res;
219 	FILE	*fd;
220 
221 	fd = fopen(NETIDFILE, "rF");
222 	if (fd == NULL) {
223 		*err = __NSW_UNAVAIL;
224 		return (0);
225 	}
226 	/*
227 	 * for each line in the file parse it appropriately
228 	 * file format is :
229 	 *	netid	uid:grp,grp,grp # for users
230 	 *	netid	0:hostname	# for hosts
231 	 */
232 	while (!feof(fd)) {
233 		res = fgets(buf, 512, fd);
234 		if (res == NULL)
235 			break;
236 
237 		/* Skip comments and blank lines */
238 		if ((*res == '#') || (*res == '\n'))
239 			continue;
240 
241 		name = &(buf[0]);
242 		while (isspace(*name))
243 			name++;
244 		if (*name == '\0')	/* blank line continue */
245 			continue;
246 		value = name;		/* will contain the value eventually */
247 		while (!isspace(*value))
248 			value++;
249 		if (*value == '\0') {
250 			syslog(LOG_WARNING,
251 				"netname2user: badly formatted line in %s.",
252 				NETIDFILE);
253 			continue;
254 		}
255 		*value++ = '\0'; /* nul terminate the name */
256 
257 		if (strcasecmp(name, netname) == 0) {
258 			(void) fclose(fd);
259 			while (isspace(*value))
260 				value++;
261 			*err = parse_netid_str(value, argp);
262 			return (*err == __NSW_SUCCESS);
263 		}
264 	}
265 	(void) fclose(fd);
266 	*err = __NSW_NOTFOUND;
267 	return (0);
268 }
269 
270 /*
271  * netname2user_nis()
272  *
273  * This function reads the netid from the NIS (YP) nameservice.
274  */
275 static int
276 netname2user_nis(int *err, char *netname, struct netid_userdata *argp)
277 {
278 	char *domain;
279 	int yperr;
280 	char *lookup;
281 	int len;
282 
283 	domain = strchr(netname, '@');
284 	if (!domain) {
285 		*err = __NSW_UNAVAIL;
286 		return (0);
287 	}
288 
289 	/* Point past the '@' character */
290 	domain++;
291 	lookup = NULL;
292 	yperr = yp_match(domain, (char *)NETID, netname, strlen(netname),
293 			&lookup, &len);
294 	switch (yperr) {
295 		case 0:
296 			break; /* the successful case */
297 
298 		default :
299 			/*
300 			 *  XXX not sure about yp_match semantics.
301 			 * should err be set to NOTFOUND here?
302 			 */
303 			*err = __NSW_UNAVAIL;
304 			return (0);
305 	}
306 	if (lookup) {
307 		lookup[len] = '\0';
308 		*err = parse_netid_str(lookup, argp);
309 		free(lookup);
310 		return (*err == __NSW_SUCCESS);
311 	}
312 	*err = __NSW_NOTFOUND;
313 	return (0);
314 }
315 
316 /*
317  * Obtain user information (uid, gidlist) from nisplus.
318  * What we're trying to do here is to map a netname into
319  * local unix information (uid, gids), relevant in
320  * the *local* domain.
321  *
322  *	 cname   auth_type auth_name public  private
323  * ----------------------------------------------------------
324  *	nisname   DES     netname   pubkey  prikey
325  *	nisname   LOCAL   uid       gidlist
326  *
327  * 1.  Find out which 'home' domain to look for user's DES entry.
328  *	This is gotten from the domain part of the netname.
329  * 2.  Get the nisplus principal name from the DES entry in the cred
330  *	table of user's home domain.
331  * 3.  Use the nisplus principal name and search in the cred table of
332  *	the *local* directory for the LOCAL entry.
333  *
334  * Note that we need this translation of netname to <uid,gidlist> to be
335  * secure, so we *must* use authenticated connections.
336  */
337 static int
338 netname2user_nisplus(int *err, char *netname, struct netid_userdata *argp)
339 {
340 	char *domain;
341 	nis_result *res;
342 	char	sname[NIS_MAXNAMELEN+1]; /*  search criteria + table name */
343 	char	principal[NIS_MAXNAMELEN+1];
344 	int len;
345 
346 	/* 1.  Get home domain of user. */
347 	domain = strchr(netname, '@');
348 	if (!domain) {
349 		*err = __NSW_UNAVAIL;
350 		return (0);
351 	}
352 	domain++;  /* skip '@' */
353 
354 
355 	/* 2.  Get user's nisplus principal name.  */
356 	if ((strlen(netname)+strlen(domain)+PKTABLE_LEN+32) >
357 		(size_t)NIS_MAXNAMELEN) {
358 		*err = __NSW_UNAVAIL;
359 		return (0);
360 	}
361 	(void) snprintf(sname, sizeof (sname),
362 		"[auth_name=\"%s\",auth_type=DES],%s.%s",
363 		netname, PKTABLE, domain);
364 	if (sname[strlen(sname) - 1] != '.')
365 		(void) strcat(sname, ".");
366 
367 	/* must use authenticated call here */
368 	/* XXX but we cant, for now. XXX */
369 	res = nis_list(sname, USE_DGRAM+NO_AUTHINFO+FOLLOW_LINKS+FOLLOW_PATH,
370 	    NULL, NULL);
371 	switch (res->status) {
372 	case NIS_SUCCESS:
373 	case NIS_S_SUCCESS:
374 		break;   /* go and do something useful */
375 	case NIS_NOTFOUND:
376 	case NIS_PARTIAL:
377 	case NIS_NOSUCHNAME:
378 	case NIS_NOSUCHTABLE:
379 		*err = __NSW_NOTFOUND;
380 		nis_freeresult(res);
381 		return (0);
382 	case NIS_S_NOTFOUND:
383 	case NIS_TRYAGAIN:
384 		*err = __NSW_TRYAGAIN;
385 		syslog(LOG_ERR,
386 			"netname2user: (nis+ lookup): %s\n",
387 			nis_sperrno(res->status));
388 		nis_freeresult(res);
389 		return (0);
390 	default:
391 		*err = __NSW_UNAVAIL;
392 		syslog(LOG_ERR, "netname2user: (nis+ lookup): %s\n",
393 			nis_sperrno(res->status));
394 		nis_freeresult(res);
395 		return (0);
396 	}
397 
398 	if (res->objects.objects_len > 1) {
399 		/*
400 		 * A netname belonging to more than one principal?
401 		 * Something wrong with cred table. should be unique.
402 		 * Warn user and continue.
403 		 */
404 		syslog(LOG_ALERT,
405 			"netname2user: DES entry for %s in \
406 			directory %s not unique",
407 			netname, domain);
408 	}
409 
410 	len = ENTRY_LEN(res->objects.objects_val, 0);
411 	(void) strncpy(principal, ENTRY_VAL(res->objects.objects_val, 0), len);
412 	principal[len] = '\0';
413 	nis_freeresult(res);
414 
415 	if (principal[0] == '\0') {
416 		*err = __NSW_UNAVAIL;
417 		return (0);
418 	}
419 
420 	/*
421 	 *	3.  Use principal name to look up uid/gid information in
422 	 *	LOCAL entry in **local** cred table.
423 	 */
424 	domain = nis_local_directory();
425 	if ((strlen(principal)+strlen(domain)+PKTABLE_LEN+30) >
426 		(size_t)NIS_MAXNAMELEN) {
427 		*err = __NSW_UNAVAIL;
428 		syslog(LOG_ERR, "netname2user: principal name '%s' too long",
429 			principal);
430 		return (0);
431 	}
432 	(void) snprintf(sname, sizeof (sname),
433 		"[cname=\"%s\",auth_type=LOCAL],%s.%s",
434 		principal, PKTABLE, domain);
435 	if (sname[strlen(sname) - 1] != '.')
436 		(void) strcat(sname, ".");
437 
438 	/* must use authenticated call here */
439 	/* XXX but we cant, for now. XXX */
440 	res = nis_list(sname, USE_DGRAM+NO_AUTHINFO+FOLLOW_LINKS+FOLLOW_PATH,
441 	    NULL, NULL);
442 	switch (res->status) {
443 	case NIS_NOTFOUND:
444 	case NIS_PARTIAL:
445 	case NIS_NOSUCHNAME:
446 	case NIS_NOSUCHTABLE:
447 		*err = __NSW_NOTFOUND;
448 		nis_freeresult(res);
449 		return (0);
450 	case NIS_S_NOTFOUND:
451 	case NIS_TRYAGAIN:
452 		*err = __NSW_TRYAGAIN;
453 		syslog(LOG_ERR,
454 			"netname2user: (nis+ lookup): %s\n",
455 			nis_sperrno(res->status));
456 		nis_freeresult(res);
457 		return (0);
458 	case NIS_SUCCESS:
459 	case NIS_S_SUCCESS:
460 		break;   /* go and do something useful */
461 	default:
462 		*err = __NSW_UNAVAIL;
463 		syslog(LOG_ERR, "netname2user: (nis+ lookup): %s\n",
464 			nis_sperrno(res->status));
465 		nis_freeresult(res);
466 		return (0);
467 	}
468 
469 	if (res->objects.objects_len > 1) {
470 		/*
471 		 * A principal can have more than one LOCAL entry?
472 		 * Something wrong with cred table.
473 		 * Warn user and continue.
474 		 */
475 		syslog(LOG_ALERT,
476 			"netname2user: LOCAL entry for %s in\
477 				directory %s not unique",
478 			netname, domain);
479 	}
480 	/* nisname	LOCAL	uid 	grp,grp,grp */
481 	*err = parse_uid_gidlist(ENTRY_VAL(res->objects.objects_val, 2),
482 					/* uid */
483 			ENTRY_VAL(res->objects.objects_val, 3), /* gids */
484 			argp);
485 	nis_freeresult(res);
486 	return (*err == __NSW_SUCCESS);
487 }
488 
489 /*
490  * Build the uid and gid from the netname for users in LDAP.
491  * There is no netid container in LDAP. For this we build
492  * the netname to user data dynamically from the passwd and
493  * group data. This works only for users in a single domain.
494  * This function is an interim solution until we support a
495  * netid container in LDAP which enables us to do netname2user
496  * resolution for multiple domains.
497  */
498 static int
499 netname2user_ldap(int *err, char *netname, struct netid_userdata *argp)
500 {
501 	char buf[NSS_LINELEN_PASSWD];
502 	char *p2, *lasts;
503 	struct passwd pw;
504 	uid_t uidnu;
505 	int ngroups = 0;
506 	int count;
507 	char pwbuf[NSS_LINELEN_PASSWD];
508 	gid_t groups[NGROUPS_MAX];
509 
510 	if (strlcpy(buf, netname, NSS_LINELEN_PASSWD) >= NSS_LINELEN_PASSWD) {
511 		*err = __NSW_UNAVAIL;
512 		return (0);
513 	}
514 
515 	/* get the uid from the netname */
516 	if (strtok_r(buf, ".", &lasts) == NULL) {
517 		*err = __NSW_UNAVAIL;
518 		return (0);
519 	}
520 	if ((p2 = strtok_r(NULL, "@", &lasts)) == NULL) {
521 		*err = __NSW_UNAVAIL;
522 		return (0);
523 	}
524 	uidnu = atoi(p2);
525 
526 	/*
527 	 * check out the primary group and crosscheck the uid
528 	 * with the passwd data
529 	 */
530 	if ((getpwuid_r(uidnu, &pw, pwbuf, sizeof (pwbuf))) == NULL) {
531 		*err = __NSW_UNAVAIL;
532 		return (0);
533 	}
534 
535 	*(argp->uidp) = pw.pw_uid;
536 	*(argp->gidp) = pw.pw_gid;
537 
538 	/* search through all groups for membership */
539 
540 	groups[0] = pw.pw_gid;
541 
542 	ngroups = _getgroupsbymember(pw.pw_name, groups, NGROUPS_MAX,
543 				(pw.pw_gid <= MAXUID) ? 1 : 0);
544 
545 	if (ngroups < 0) {
546 		*err = __NSW_UNAVAIL;
547 		return (0);
548 	}
549 
550 	*(argp->gidlenp) = ngroups;
551 
552 	for (count = 0; count < ngroups; count++) {
553 		(argp->gidlist[count]) = groups[count];
554 	}
555 
556 	*err = __NSW_SUCCESS;
557 	return (1);
558 
559 }
560 
561 /*
562  * Convert network-name into unix credential
563  */
564 int
565 netname2user(const char netname[MAXNETNAMELEN + 1], uid_t *uidp, gid_t *gidp,
566 						int *gidlenp, gid_t *gidlist)
567 {
568 	struct __nsw_switchconfig *conf;
569 	struct __nsw_lookup *look;
570 	enum __nsw_parse_err perr;
571 	int needfree = 1, res;
572 	struct netid_userdata argp;
573 	int err;
574 
575 	/*
576 	 * Take care of the special case of nobody. Compare the netname
577 	 * to the string "nobody". If they are equal, return the SVID
578 	 * standard value for nobody.
579 	 */
580 
581 	if (strcmp(netname, "nobody") == 0) {
582 		*uidp = NOBODY_UID;
583 		*gidp = NOBODY_UID;
584 		*gidlenp = 0;
585 		return (1);
586 	}
587 
588 	/*
589 	 * First we do some generic sanity checks on the name we were
590 	 * passed. This lets us assume they are correct in the backends.
591 	 *
592 	 * NOTE: this code only recognizes names of the form :
593 	 *		unix.UID@domainname
594 	 */
595 	if (strncmp(netname, OPSYS, OPSYS_LEN) != 0)
596 		return (0);
597 	if (!isdigit(netname[OPSYS_LEN+1]))	/* check for uid string */
598 		return (0);
599 
600 	argp.uidp = uidp;
601 	argp.gidp = gidp;
602 	argp.gidlenp = gidlenp;
603 	argp.gidlist = gidlist;
604 	(void) mutex_lock(&serialize_netname_r);
605 
606 	conf = __nsw_getconfig("publickey", &perr);
607 	if (!conf) {
608 		conf = &publickey_default;
609 		needfree = 0;
610 	} else
611 		needfree = 1; /* free the config structure */
612 
613 	for (look = conf->lookups; look; look = look->next) {
614 		if (strcmp(look->service_name, "nisplus") == 0)
615 			res = netname2user_nisplus(&err,
616 						(char *)netname, &argp);
617 		else if (strcmp(look->service_name, "nis") == 0)
618 			res = netname2user_nis(&err, (char *)netname, &argp);
619 		else if (strcmp(look->service_name, "files") == 0)
620 			res = netname2user_files(&err, (char *)netname, &argp);
621 		else if (strcmp(look->service_name, "ldap") == 0)
622 			res = netname2user_ldap(&err, (char *)netname, &argp);
623 		else {
624 			syslog(LOG_INFO,
625 		"netname2user: unknown nameservice for publickey info '%s'\n",
626 						look->service_name);
627 			err = __NSW_UNAVAIL;
628 		}
629 		switch (look->actions[err]) {
630 			case __NSW_CONTINUE :
631 				break;
632 			case __NSW_RETURN :
633 				if (needfree)
634 					__nsw_freeconfig(conf);
635 				(void) mutex_unlock(&serialize_netname_r);
636 				return (res);
637 			default :
638 				syslog(LOG_ERR,
639 			"netname2user: Unknown action for nameservice '%s'",
640 							look->service_name);
641 		}
642 	}
643 	if (needfree)
644 		__nsw_freeconfig(conf);
645 	(void) mutex_unlock(&serialize_netname_r);
646 	return (0);
647 }
648 
649 /*
650  * Convert network-name to hostname (fully qualified)
651  * NOTE: this code only recognizes names of the form :
652  *		unix.HOST@domainname
653  *
654  * This is very simple.  Since the netname is of the form:
655  *	unix.host@domainname
656  * We just construct the hostname using information from the domainname.
657  */
658 int
659 netname2host(const char netname[MAXNETNAMELEN + 1], char *hostname,
660 							const int hostlen)
661 {
662 	char *p, *domainname;
663 	int len, dlen;
664 
665 	if (!netname) {
666 		syslog(LOG_ERR, "netname2host: null netname");
667 		goto bad_exit;
668 	}
669 
670 	if (strncmp(netname, OPSYS, OPSYS_LEN) != 0)
671 		goto bad_netname;
672 	p = (char *)netname + OPSYS_LEN;	/* skip OPSYS part */
673 	if (*p != '.')
674 		goto bad_netname;
675 	++p;				/* skip '.' */
676 
677 	domainname = strchr(p, '@');	/* get domain name */
678 	if (domainname == 0)
679 		goto bad_netname;
680 
681 	len = domainname - p;		/* host sits between '.' and '@' */
682 	domainname++;			/* skip '@' sign */
683 
684 	if (len <= 0)
685 		goto bad_netname;
686 
687 	if (hostlen < len) {
688 		syslog(LOG_ERR,
689 			"netname2host: insufficient space for hostname");
690 		goto bad_exit;
691 	}
692 
693 	if (isdigit(*p))		/* don't want uid here */
694 		goto bad_netname;
695 
696 	if (*p == '\0')			/* check for null hostname */
697 		goto bad_netname;
698 
699 	(void) strncpy(hostname, p, len);
700 
701 	/* make into fully qualified hostname by concatenating domain part */
702 	dlen = strlen(domainname);
703 	if (hostlen < (len + dlen + 2)) {
704 		syslog(LOG_ERR,
705 			"netname2host: insufficient space for hostname");
706 		goto bad_exit;
707 	}
708 
709 	hostname[len] = '.';
710 	(void) strncpy(hostname+len+1, domainname, dlen);
711 	hostname[len+dlen+1] = '\0';
712 
713 	return (1);
714 
715 bad_netname:
716 	syslog(LOG_ERR, "netname2host: invalid host netname %s", netname);
717 
718 bad_exit:
719 	hostname[0] = '\0';
720 	return (0);
721 }
722