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