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