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, Version 1.0 only
6 * (the "License"). You may not use this file except in compliance
7 * with the License.
8 *
9 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10 * or http://www.opensolaris.org/os/licensing.
11 * See the License for the specific language governing permissions
12 * and limitations under the License.
13 *
14 * When distributing Covered Code, include this CDDL HEADER in each
15 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16 * If applicable, add the following below this CDDL HEADER, with the
17 * fields enclosed by brackets "[]" replaced with your own identifying
18 * information: Portions Copyright [yyyy] [name of copyright owner]
19 *
20 * CDDL HEADER END
21 */
22 /*
23 * Copyright 2004 Sun Microsystems, Inc. All rights reserved.
24 * Use is subject to license terms.
25 */
26
27 #include <stdio.h>
28 #include <stdlib.h>
29 #include <strings.h>
30 #include <string.h>
31 #include <unistd.h>
32 #include <pthread.h>
33 #include <sys/types.h>
34 #include <sys/socket.h>
35 #include <netinet/in.h>
36 #include <arpa/inet.h>
37
38 #include "ldap_util.h"
39 #include "ldap_glob.h"
40
41 static time_t msgtime[MSG_LASTMSG] = {0};
42 static time_t msgtimeout = 3600;
43
44 static pthread_key_t tsdKey;
45
46 /*
47 * Log a message to the appropriate place.
48 */
49 void
logmsg(int msgtype,int priority,const char * fmt,...)50 logmsg(int msgtype, int priority, const char *fmt, ...) {
51 va_list ap;
52 struct timeval tp;
53
54 /*
55 * Only log LOG_INFO priority if 'verbose' is on, or if
56 * msgtype is MSG_ALWAYS.
57 */
58 if (priority == LOG_INFO && !verbose && msgtype != MSG_ALWAYS)
59 return;
60
61 /* Make sure we don't log the same message too often */
62 if (msgtype != MSG_NOTIMECHECK && msgtype != MSG_ALWAYS &&
63 msgtype > 0 && msgtype < MSG_LASTMSG &&
64 gettimeofday(&tp, 0) != -1) {
65 if (tp.tv_sec - msgtime[msgtype] < msgtimeout)
66 return;
67 msgtime[msgtype] = tp.tv_sec;
68 }
69
70 va_start(ap, fmt);
71 if (cons == 0) {
72 vsyslog(priority, fmt, ap);
73 } else {
74 int flen = slen(fmt);
75
76 vfprintf(cons, fmt, ap);
77 /*
78 * If the last character in 'fmt' wasn't a '\n', write one
79 * to the console.
80 */
81 if (flen > 0 && fmt[flen-1] != '\n')
82 fprintf(cons, "\n");
83 }
84 va_end(ap);
85 }
86
87 void
__destroyTsdKey(void * arg)88 __destroyTsdKey(void *arg) {
89 __nis_deferred_error_t *defErr = arg;
90
91 if (defErr != 0) {
92 sfree(defErr->message);
93 free(defErr);
94 }
95 }
96
97 static void
__initTsdKey(void)98 __initTsdKey(void)
99 {
100 (void) pthread_key_create(&tsdKey, __destroyTsdKey);
101 }
102 #pragma init(__initTsdKey)
103
104 void
reportError(int error,char * fmt,...)105 reportError(int error, char *fmt, ...) {
106 __nis_deferred_error_t *defErr = pthread_getspecific(tsdKey);
107 int doStore = (defErr == 0);
108 char *myself = "reportError";
109 va_list ap;
110 __nis_buffer_t b = {0, 0};
111
112 if (defErr == 0 && (defErr = am(myself, sizeof (*defErr))) == 0)
113 return;
114
115 va_start(ap, fmt);
116 b.len = vp2buf(myself, &b.buf, b.len, fmt, ap);
117 va_end(ap);
118
119 if (b.len > 0) {
120 defErr->error = error;
121 defErr->message = b.buf;
122 if (doStore) {
123 int ret = pthread_setspecific(tsdKey, defErr);
124 if (ret != 0) {
125 logmsg(MSG_TSDERR, LOG_ERR,
126 "%s: pthread_setspecific() => %d",
127 myself, ret);
128 sfree(b.buf);
129 free(defErr);
130 }
131 }
132 }
133 }
134
135 int
getError(char ** message)136 getError(char **message) {
137 __nis_deferred_error_t *defErr = pthread_getspecific(tsdKey);
138 char *myself = "getError";
139
140 if (defErr == 0) {
141 if (message != 0)
142 *message = sdup(myself, T, "no TSD");
143 return (NPL_TSDERR);
144 }
145
146 if (message != 0)
147 *message = sdup(myself, T, defErr->message);
148
149 return (defErr->error);
150 }
151
152 void
clearError(void)153 clearError(void) {
154 __nis_deferred_error_t *defErr = pthread_getspecific(tsdKey);
155
156 if (defErr != 0) {
157 sfree(defErr->message);
158 defErr->message = 0;
159 defErr->error = NPL_NOERROR;
160 }
161 }
162
163 void
logError(int priority)164 logError(int priority) {
165 __nis_deferred_error_t *defErr = pthread_getspecific(tsdKey);
166 int msgtype;
167
168 if (defErr != 0) {
169 switch (defErr->error) {
170 case NPL_NOERROR:
171 msgtype = MSG_LASTMSG;
172 break;
173 case NPL_NOMEM:
174 msgtype = MSG_NOMEM;
175 break;
176 case NPL_TSDERR:
177 msgtype = MSG_TSDERR;
178 break;
179 case NPL_BERENCODE:
180 case NPL_BERDECODE:
181 msgtype = MSG_BER;
182 break;
183 default:
184 msgtype = MSG_LASTMSG;
185 break;
186 }
187
188 if (msgtype != MSG_LASTMSG) {
189 logmsg(msgtype, priority, defErr->message);
190 }
191 }
192 }
193
194 /*
195 * Allocate zero-initialized memory of the specified 'size'. If the
196 * allocation fails, log a message and return NULL. Allocation of
197 * zero bytes is legal, and returns a NULL pointer.
198 */
199 void *
am(const char * msg,int size)200 am(const char *msg, int size) {
201 void *p;
202
203 if (size > 0) {
204 p = calloc(1, size);
205 if (p == 0) {
206 if (msg == 0)
207 msg = "<unknown>";
208 logmsg(MSG_NOMEM, LOG_ERR, "%s: calloc(%d) => NULL\n",
209 msg, size);
210 return (0);
211 }
212 } else if (size == 0) {
213 p = 0;
214 } else {
215 if (msg == 0)
216 msg = "<unknown>";
217 logmsg(MSG_MEMPARAM, LOG_INFO, "%s: size (%d) < 0\n", size);
218 exit(-1);
219 }
220 return (p);
221 }
222
223 /*
224 * Return the length of a string, just like strlen(), but don't croak
225 * on a NULL pointer.
226 */
227 int
slen(const char * str)228 slen(const char *str) {
229 return ((str != 0) ? strlen(str) : 0);
230 }
231
232 /*
233 * If allocate==0, return 'str'; othewise, duplicate the string just
234 * like strdup(), but don't die if 'str' is a NULL pointer.
235 */
236 char *
sdup(const char * msg,int allocate,char * str)237 sdup(const char *msg, int allocate, char *str) {
238 char *s;
239
240 if (!allocate)
241 return (str);
242
243 if (str == 0) {
244 s = strdup("");
245 } else {
246 s = strdup(str);
247 }
248 if (s == 0) {
249 logmsg(MSG_NOMEM, LOG_ERR, "%s: strdup(%d bytes) => NULL\n",
250 (msg != 0) ? msg : "<unknown>", slen(str)+1);
251 }
252 return (s);
253 }
254
255 /*
256 * Concatenate strings like strcat(), but don't expire if passed a
257 * NULL pointer or two. If deallocate!=0, free() the input strings.
258 */
259 char *
scat(const char * msg,int deallocate,char * s1,char * s2)260 scat(const char *msg, int deallocate, char *s1, char *s2) {
261 char *n;
262 int l1 = 0, l2 = 0;
263
264 if (s1 == 0) {
265 n = sdup(msg, T, s2);
266 if (deallocate)
267 sfree(s2);
268 return (n);
269 } else if (s2 == 0) {
270 n = sdup(msg, T, s1);
271 if (deallocate)
272 free(s1);
273 return (n);
274 }
275
276 l1 = strlen(s1);
277 l2 = strlen(s2);
278
279 n = malloc(l1+l2+1);
280 if (n != 0) {
281 memcpy(n, s1, l1);
282 memcpy(&n[l1], s2, l2);
283 n[l1+l2] = '\0';
284 } else {
285 logmsg(MSG_NOMEM, LOG_ERR, "%s: malloc(%d) => NULL\n",
286 (msg != 0) ? msg : "<unknown>", l1+l2+1);
287 }
288
289 if (deallocate) {
290 free(s1);
291 free(s2);
292 }
293
294 return (n);
295 }
296
297 /* For debugging */
298 static void *PTR = 0;
299
300 /*
301 * Counters for memory errors. Note that we don't protect access,
302 * so the values aren't entirely reliable in an MT application.
303 */
304 ulong_t numMisaligned = 0;
305 ulong_t numNotActive = 0;
306
307 /* free() the input, but don't pass away if it's NULL */
308 void
sfree(void * ptr)309 sfree(void *ptr) {
310
311 /* NULL pointer OK */
312 if (ptr == 0)
313 return;
314
315 /*
316 * For use in the debugger, when we need to detect free of a
317 * certain address.
318 */
319 if (ptr == PTR)
320 abort();
321
322 /*
323 * All addresses returned by malloc() and friends are "suitably
324 * aligned for any use", so they should fall on eight-byte boundaries.
325 */
326 if (((unsigned long)ptr % 8) != 0) {
327 numMisaligned++;
328 return;
329 }
330
331 #ifdef NISDB_LDAP_DEBUG
332 /*
333 * Malloc:ed memory should have the length (four bytes), starting
334 * eight bytes before the block, and with the least-significant
335 * bit set.
336 */
337 if ((((uint_t *)ptr)[-2] & 0x1) == 0) {
338 numNotActive++;
339 return;
340 }
341 #endif /* NISDB_LDAP_DEBUG */
342
343 /* Finally, we believe it's OK to free() the pointer */
344 free(ptr);
345 }
346
347 /*
348 * If a __nis_single_value_t represents a string, the length count may or may
349 * not include a concluding NUL. Hence this function, which returns the last
350 * non-NUL character of the value.
351 */
352 char
lastChar(__nis_single_value_t * v)353 lastChar(__nis_single_value_t *v) {
354 char *s;
355
356 if (v == 0 || v->value == 0 || v->length < 2)
357 return ('\0');
358
359 s = v->value;
360 if (s[v->length - 1] != '\0')
361 return (s[v->length - 1]);
362 else
363 return (s[v->length - 2]);
364 }
365
366 void *
appendString2SingleVal(char * str,__nis_single_value_t * v,int * newLen)367 appendString2SingleVal(char *str, __nis_single_value_t *v, int *newLen) {
368 void *s;
369 int l, nl;
370 char *myself = "appendString2SingleVal";
371
372 if (v == 0 || v->length < 0)
373 return (0);
374
375 /*
376 * If 'str' is NULL or empty, just return NULL so that the caller
377 * does nothing.
378 */
379 l = slen(str);
380 if (l <= 0)
381 return (0);
382
383 s = am(myself, (nl = l + v->length) + 1);
384 if (s == 0) {
385 /* Caller does nothing; let's hope for the best... */
386 return (0);
387 }
388
389 if (v->value != 0)
390 memcpy(s, v->value, v->length);
391
392 memcpy(&(((char *)s)[v->length]), str, l);
393
394 if (newLen != 0)
395 *newLen = nl;
396
397 return (s);
398 }
399
400
401 /*
402 * Do the equivalent of a strcmp() between a string and a string-valued
403 * __nis_single_value_t.
404 */
405 int
scmp(char * s,__nis_single_value_t * v)406 scmp(char *s, __nis_single_value_t *v) {
407
408 if (s == 0)
409 return (1);
410 else if (v == 0 || v->value == 0 || v->length <= 0)
411 return (-1);
412
413 return (strncmp(s, v->value, v->length));
414 }
415
416 /*
417 * Do the equivalent of a strcasecmp() between a string and a string-valued
418 * __nis_single_value_t.
419 */
420 int
scasecmp(char * s,__nis_single_value_t * v)421 scasecmp(char *s, __nis_single_value_t *v) {
422
423 if (s == 0)
424 return (1);
425 else if (v == 0 || v->value == 0 || v->length <= 0)
426 return (-1);
427
428 return (strncasecmp(s, v->value, v->length));
429 }
430
431 #define STDBUFSIZE 81
432
433 /*
434 * vsprintf the 'fmt' and 'ap' to a buffer, then concatenate the
435 * result to '*buf'.
436 */
437 int
vp2buf(const char * msg,char ** buf,int buflen,const char * fmt,va_list ap)438 vp2buf(const char *msg, char **buf, int buflen, const char *fmt, va_list ap) {
439 char *newbuf = am(msg, STDBUFSIZE);
440 int size = 0;
441
442 if (newbuf == 0)
443 return (0);
444
445 if (buf == 0 || buflen < 0 || fmt == 0) {
446 free(newbuf);
447 return (0);
448 }
449
450 /* Find out how large the new buffer needs to be */
451 size = vsnprintf(newbuf, STDBUFSIZE, fmt, ap);
452
453 if (size > STDBUFSIZE) {
454 free(newbuf);
455 newbuf = am(msg, size+1);
456 if (newbuf == 0)
457 return (0);
458 size = vsnprintf(newbuf, size+1, fmt, ap);
459 }
460
461 *buf = scat(msg, T, *buf, newbuf);
462 /* Don't count the NUL. This enables us to concatenate correctly */
463 buflen += size;
464
465 return (buflen);
466 }
467
468 /* Generic print buffer */
469 __nis_buffer_t pb = {0, 0};
470
471 /* sprintf to the generic __nis_buffer_t */
472 void
p2buf(char * msg,char * fmt,...)473 p2buf(char *msg, char *fmt, ...) {
474 va_list ap;
475
476 va_start(ap, fmt);
477 pb.len = vp2buf(msg, &pb.buf, pb.len, fmt, ap);
478 va_end(ap);
479 }
480
481 /* sprintf to the specified __nis_buffer_t */
482 void
bp2buf(const char * msg,__nis_buffer_t * b,const char * fmt,...)483 bp2buf(const char *msg, __nis_buffer_t *b, const char *fmt, ...) {
484 va_list ap;
485
486 va_start(ap, fmt);
487 b->len = vp2buf(msg, &b->buf, b->len, fmt, ap);
488 va_end(ap);
489 }
490
491 /* Copy 'buf' to the specified __nis_buffer_t */
492 void
bc2buf(const char * msg,void * buf,int len,__nis_buffer_t * b)493 bc2buf(const char *msg, void *buf, int len, __nis_buffer_t *b) {
494 void *new;
495
496 /*
497 * Make buffer one byte larger than the lenghts indicate. This
498 * gives us room to append a NUL, so that we can mix string and
499 * non-string copies into the buffer, and still end up with
500 * something that can be sent to printf(), strcat(), etc.
501 */
502 new = realloc(b->buf, b->len+len+1);
503 if (new != 0) {
504 b->buf = new;
505 memcpy(&(b->buf[b->len]), buf, len);
506 b->len += len;
507 /* Put a NUL at the end, just in case we printf() */
508 if (b->len > 0 && b->buf[b->len-1] != '\0')
509 b->buf[b->len] = '\0';
510 } else {
511 logmsg(MSG_NOMEM, LOG_ERR, "%s: realloc(%d) => NULL\n",
512 (msg != 0) ? msg : "<unknown", b->len+len);
513 }
514 }
515
516 /* Like bc2buf(), but remove any trailing NUL bytes */
517 void
sbc2buf(const char * msg,void * buf,int len,__nis_buffer_t * b)518 sbc2buf(const char *msg, void *buf, int len, __nis_buffer_t *b) {
519 if (buf == 0 || len <= 0 || b == 0)
520 return;
521 /* Snip off trailing NULs */
522 while (len > 0 && ((char *)buf)[len-1] == '\0')
523 len--;
524 if (len <= 0)
525 return;
526 bc2buf(msg, buf, len, b);
527 }
528
529 /* Copy 'buf' to the generic __nis_buffer_t */
530 void
c2buf(char * msg,void * buf,int len)531 c2buf(char *msg, void *buf, int len) {
532 bc2buf(msg, buf, len, &pb);
533 }
534
535 /* Like c2buf(), but remove trailing NUL bytes */
536 void
sc2buf(char * msg,void * buf,int len)537 sc2buf(char *msg, void *buf, int len) {
538 sbc2buf(msg, buf, len, &pb);
539 }
540
541 /* How many times we try write(2) if it fails */
542 #define MAXTRY 10
543
544 /* Output the generic __nis_buffer_t to stdout */
545 void
printbuf(void)546 printbuf(void) {
547 int maxtry = MAXTRY, len = pb.len;
548
549 if (pb.buf != 0) {
550 int tmp;
551
552 while (len > 0 && maxtry > 0) {
553 tmp = write(1, pb.buf, len);
554 if (tmp < 0)
555 break;
556 len -= tmp;
557 if (tmp > 0)
558 maxtry = MAXTRY;
559 else
560 maxtry--;
561 }
562 free(pb.buf);
563 pb.buf = 0;
564 }
565 pb.len = 0;
566 }
567
568 void *
extendArray(void * array,int newsize)569 extendArray(void *array, int newsize) {
570 void *new = realloc(array, newsize);
571 if (new == 0)
572 sfree(array);
573 return (new);
574 }
575
576 /*
577 * Determine if the given string is an IP address (IPv4 or IPv6).
578 * If so, it converts it to the format as required by rfc2307bis
579 * and *newaddr will point to the new Address.
580 *
581 * Returns -2 : error
582 * -1 : not an IP address
583 * 0 : IP address not supported by rfc2307bis
584 * AF_INET : IPv4
585 * AF_INET6 : IPv6
586 */
587 int
checkIPaddress(char * addr,int len,char ** newaddr)588 checkIPaddress(char *addr, int len, char **newaddr) {
589 ipaddr_t addr_ipv4;
590 in6_addr_t addr_ipv6;
591 char *buffer;
592 int s, e;
593 char *myself = "checkIPaddress";
594
595 /* skip leading whitespaces */
596 for (s = 0; (s < len) && (addr[s] == ' ' || addr[s] == '\t'); s++);
597 if (s >= len)
598 return (-1);
599
600 /* skip trailing whitespaces */
601 for (e = len - 1; (e > s) && (addr[e] == ' ' || addr[e] == '\t'); e--);
602 if (s == e)
603 return (-1);
604
605 /* adjust len */
606 len = e - s + 1;
607
608 if ((buffer = am(myself, len + 1)) == 0)
609 return (-2);
610 (void) memcpy(buffer, addr + s, len);
611
612 if (inet_pton(AF_INET6, buffer, &addr_ipv6) == 1) {
613 sfree(buffer);
614 /*
615 * IPv4-compatible IPv6 address and IPv4-mapped
616 * IPv6 addresses not allowed by rfc2307bis
617 */
618 if (IN6_IS_ADDR_V4COMPAT(&addr_ipv6))
619 return (0);
620 if (IN6_IS_ADDR_V4MAPPED(&addr_ipv6))
621 return (0);
622 if (newaddr == 0)
623 return (AF_INET6);
624 if ((*newaddr = am(myself, INET6_ADDRSTRLEN)) == 0)
625 return (-2);
626 if (inet_ntop(AF_INET6, &addr_ipv6, *newaddr, INET6_ADDRSTRLEN))
627 return (AF_INET6);
628 sfree(*newaddr);
629 return (-2);
630 }
631
632 if (inet_pton(AF_INET, buffer, &addr_ipv4) == 1) {
633 sfree(buffer);
634 if (newaddr == 0)
635 return (AF_INET);
636 if ((*newaddr = am(myself, INET_ADDRSTRLEN)) == 0)
637 return (-2);
638 if (inet_ntop(AF_INET, &addr_ipv4, *newaddr, INET_ADDRSTRLEN))
639 return (AF_INET);
640 sfree(*newaddr);
641 return (-2);
642 }
643
644 sfree(buffer);
645 return (-1);
646 }
647
648 int
sstrncmp(const char * s1,const char * s2,int n)649 sstrncmp(const char *s1, const char *s2, int n) {
650 if (s1 == 0 && s2 == 0)
651 return (0);
652
653 if (s1 == 0)
654 return (1);
655
656 if (s2 == 0)
657 return (-1);
658
659 return (strncmp(s1, s2, n));
660 }
661
662 /*
663 * Does the following:
664 * - Trims leading and trailing whitespaces
665 * - Collapses two or more whitespaces into one space
666 * - Converts all whitespaces into spaces
667 * - At entrance, *len contains length of str
668 * - At exit, *len will contain length of the return string
669 * - In case of mem alloc failure, *len should be ignored
670 */
671 char *
trimWhiteSpaces(char * str,int * len,int deallocate)672 trimWhiteSpaces(char *str, int *len, int deallocate) {
673 char *ostr;
674 int olen = 0;
675 int first = 1, i;
676 char *myself = "trimWhiteSpaces";
677
678 if ((ostr = am(myself, *len + 1)) == 0) {
679 if (deallocate)
680 sfree(str);
681 *len = 0;
682 return (0);
683 }
684
685 /* Skip leading whitespaces */
686 for (i = 0; i < *len && (str[i] == ' ' || str[i] == '\t'); i++);
687
688 /* Collapse multiple whitespaces into one */
689 for (; i < *len; i++) {
690 if (str[i] == ' ' || str[i] == '\t') {
691 if (first) {
692 first = 0;
693 ostr[olen++] = ' ';
694 }
695 continue;
696 }
697 first = 1;
698 ostr[olen++] = str[i];
699 }
700
701 /* Handle the trailing whitespace if any */
702 if (olen && ostr[olen - 1] == ' ') {
703 olen--;
704 ostr[olen] = 0;
705 }
706
707 if (deallocate)
708 sfree(str);
709
710 *len = olen;
711 return (ostr);
712 }
713
714 /*
715 * Escapes special characters in DN using the list from RFC 2253
716 */
717 int
escapeSpecialChars(__nis_value_t * val)718 escapeSpecialChars(__nis_value_t *val) {
719 int i, j, k, count;
720 char *newval, *s;
721 char *myself = "escapeSpecialChars";
722
723 /* Assume val is always non NULL */
724
725 for (i = 0; i < val->numVals; i++) {
726 /*
727 * Count the special characters in value to determine
728 * the length for the new value
729 */
730 s = val->val[i].value;
731 for (j = 0, count = 0; j < val->val[i].length; j++, s++) {
732 if (*s == '#' || *s == ',' || *s == '+' || *s == '"' ||
733 *s == '\\' || *s == '<' || *s == '>' || *s == ';')
734 count++;
735 }
736 if (count == 0)
737 continue;
738
739 if ((newval = am(myself, val->val[i].length + count + 1)) == 0)
740 return (-1);
741
742 /* Escape the special characters using '\\' */
743 s = val->val[i].value;
744 for (j = 0, k = 0; j < val->val[i].length; j++, k++, s++) {
745 if (*s == '#' || *s == ',' || *s == '+' || *s == '"' ||
746 *s == '\\' || *s == '<' || *s == '>' || *s == ';')
747 newval[k++] = '\\';
748 newval[k] = *s;
749 }
750
751 sfree(val->val[i].value);
752 val->val[i].value = newval;
753 val->val[i].length += count;
754 }
755
756 return (1);
757 }
758
759 /*
760 * Remove escape characters from DN returned by LDAP server
761 */
762 void
removeEscapeChars(__nis_value_t * val)763 removeEscapeChars(__nis_value_t *val) {
764 int i;
765 char *s, *d, *end;
766
767
768 for (i = 0; i < val->numVals; i++) {
769 s = val->val[i].value;
770 end = s + val->val[i].length;
771
772 /*
773 * This function is called frequently and for most entries
774 * there will be no escapes. Process rapidly up to first escape.
775 */
776 for (d = s; s < end; s++, d++) {
777 if (*s == '\\')
778 break;
779 }
780
781 /*
782 * Reached the end, in which case will not go into loop,
783 * or found an escape and now have to start moving data.
784 */
785 for (; s < end; s++) {
786 if (*s == '\\') {
787 val->val[i].length--;
788 /*
789 * Next character gets coppied without being
790 * checked
791 */
792 s++;
793 if (s >= end)
794 break;
795 }
796
797 *d = *s;
798 d++;
799 }
800 }
801 }
802