1 /* helpers.c - Various helper functions
2
3 Copyright (C) 2000, 2001 Thomas Moestl
4 Copyright (C) 2002, 2003, 2005, 2006, 2008, 2011 Paul A. Rombouts
5
6 This file is part of the pdnsd package.
7
8 pdnsd is free software; you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation; either version 3 of the License, or
11 (at your option) any later version.
12
13 pdnsd is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
17
18 You should have received a copy of the GNU General Public License
19 along with pdnsd; see the file COPYING. If not, see
20 <http://www.gnu.org/licenses/>.
21 */
22
23 #include <config.h>
24 #include <sys/types.h>
25 #include <sys/time.h>
26 #include <signal.h>
27 #include <stdlib.h>
28 #include <stdarg.h>
29 #include <ctype.h>
30 #include <stdio.h>
31 #include <string.h>
32 #include <unistd.h>
33 #include <pwd.h>
34 #include <grp.h>
35 #include <errno.h>
36 #include "ipvers.h"
37 #include "thread.h"
38 #include "error.h"
39 #include "helpers.h"
40 #include "cache.h"
41 #include "conff.h"
42
43
44 /*
45 * This is to exit pdnsd from any thread.
46 */
pdnsd_exit()47 void pdnsd_exit()
48 {
49 pthread_kill(main_thrid,SIGTERM);
50 pthread_exit(NULL);
51 }
52
53 /*
54 * Try to grab a mutex. If we can't, fail. This will loop until we get the
55 * mutex or fail. This is only used in debugging code or at exit, otherwise
56 * we might run into lock contention problems.
57 */
softlock_mutex(pthread_mutex_t * mutex)58 int softlock_mutex(pthread_mutex_t *mutex)
59 {
60 unsigned int tr=0;
61 while(pthread_mutex_trylock(mutex)) {
62 if (++tr>=SOFTLOCK_MAXTRIES)
63 return 0;
64 usleep_r(10000);
65 }
66 return 1;
67 }
68
69 /*
70 * setuid() and setgid() for a specified user.
71 */
run_as(const char * user)72 int run_as(const char *user)
73 {
74 if (user[0]) {
75 #ifdef HAVE_GETPWNAM_R
76 struct passwd pwdbuf, *pwd;
77 size_t buflen;
78 int err;
79
80 for(buflen=128;; buflen*=2) {
81 char buf[buflen]; /* variable length array */
82
83 /* Note that we use getpwnam_r() instead of getpwnam(),
84 which returns its result in a statically allocated buffer and
85 cannot be considered thread safe.
86 Doesn't use NSS! */
87 err=getpwnam_r(user, &pwdbuf, buf, buflen, &pwd);
88 if(err==0 && pwd) {
89 /* setgid first, because we may not be allowed to do it anymore after setuid */
90 if (setgid(pwd->pw_gid)!=0) {
91 log_error("Could not change group id to that of run_as user '%s': %s",
92 user,strerror(errno));
93 return 0;
94 }
95
96 /* initgroups uses NSS, so we can disable it,
97 i.e. we might need DNS for LDAP lookups, which times out */
98 if (global.use_nss && (initgroups(user, pwd->pw_gid)!=0)) {
99 log_error("Could not initialize the group access list of run_as user '%s': %s",
100 user,strerror(errno));
101 return 0;
102 }
103 if (setuid(pwd->pw_uid)!=0) {
104 log_error("Could not change user id to that of run_as user '%s': %s",
105 user,strerror(errno));
106 return 0;
107 }
108 break;
109 }
110 else if(err!=ERANGE) {
111 if(err)
112 log_error("run_as user '%s' could not be found: %s",user,strerror(err));
113 else
114 log_error("run_as user '%s' could not be found.",user);
115 return 0;
116 }
117 else if(buflen>=16*1024) {
118 /* If getpwnam_r() seems defective, call it quits rather than
119 keep on allocating ever larger buffers until we crash. */
120 log_error("getpwnam_r() requires more than %u bytes of buffer space.",(unsigned)buflen);
121 return 0;
122 }
123 /* Else try again with larger buffer. */
124 }
125 #else
126 /* No getpwnam_r() :-( We'll use getpwnam() and hope for the best. */
127 struct passwd *pwd;
128
129 if (!(pwd=getpwnam(user))) {
130 log_error("run_as user %s could not be found.",user);
131 return 0;
132 }
133 /* setgid first, because we may not allowed to do it anymore after setuid */
134 if (setgid(pwd->pw_gid)!=0) {
135 log_error("Could not change group id to that of run_as user '%s': %s",
136 user,strerror(errno));
137 return 0;
138 }
139 /* initgroups uses NSS, so we can disable it,
140 i.e. we might need DNS for LDAP lookups, which times out */
141 if (global.use_nss && (initgroups(user, pwd->pw_gid)!=0)) {
142 log_error("Could not initialize the group access list of run_as user '%s': %s",
143 user,strerror(errno));
144 return 0;
145 }
146 if (setuid(pwd->pw_uid)!=0) {
147 log_error("Could not change user id to that of run_as user '%s': %s",
148 user,strerror(errno));
149 return 0;
150 }
151 #endif
152 }
153
154 return 1;
155 }
156
157 /*
158 * returns whether c is allowed in IN domain names
159 */
160 #if 0
161 int isdchar (unsigned char c)
162 {
163 if ((c>='a' && c<='z') || (c>='A' && c<='Z') || (c>='0' && c<='9') || c=='-'
164 #ifdef UNDERSCORE
165 || c=='_'
166 #endif
167 )
168 return 1;
169 return 0;
170 }
171 #endif
172
173 /*
174 * Convert a string given in dotted notation to the transport format (length byte prepended
175 * domain name parts, ended by a null length sequence)
176 * The memory areas referenced by str and rhn may not overlap.
177 * The buffer rhn points to is assumed to be at least DNSNAMEBUFSIZE bytes in size.
178 *
179 * Returns 1 if successful, otherwise 0.
180 */
str2rhn(const unsigned char * str,unsigned char * rhn)181 int str2rhn(const unsigned char *str, unsigned char *rhn)
182 {
183 unsigned int i,j;
184
185 if(*str=='.' && !*(str+1)) {
186 /* Special case: root domain */
187 rhn[0]=0;
188 return 1;
189 }
190
191 for(i=0;;) {
192 unsigned int jlim= i+63;
193 if(jlim>DNSNAMEBUFSIZE-2) jlim=DNSNAMEBUFSIZE-2; /* DNSNAMEBUFSIZE-2 because the termination 0 has to follow */
194 for(j=i; str[j] && str[j]!='.'; ++j) {
195 if(j>=jlim) return 0;
196 rhn[j+1]=str[j];
197 }
198 if(!str[j])
199 break;
200 if(j<=i)
201 return 0;
202 rhn[i]=(unsigned char)(j-i);
203 i = j+1;
204 }
205
206 rhn[i]=0;
207 if (j>i || i==0)
208 return 0;
209 return 1;
210 }
211
212 /*
213 parsestr2rhn is essentially the same as str2rhn, except that it doesn't look beyond
214 the first len chars in the input string. It also tolerates strings
215 not ending in a dot and returns a message in case of an error.
216 */
parsestr2rhn(const unsigned char * str,unsigned int len,unsigned char * rhn)217 const char *parsestr2rhn(const unsigned char *str, unsigned int len, unsigned char *rhn)
218 {
219 unsigned int i,j;
220
221 if(len>0 && *str=='.' && (len==1 || !*(str+1))) {
222 /* Special case: root domain */
223 rhn[0]=0;
224 return NULL;
225 }
226
227 i=0;
228 do {
229 unsigned int jlim= i+63;
230 if(jlim>DNSNAMEBUFSIZE-2) jlim=DNSNAMEBUFSIZE-2;
231 for(j=i; j<len && str[j] && str[j]!='.'; ++j) {
232 /* if(!isdchar(str[j]))
233 return "Illegal character in domain name"; */
234 if(j>=jlim)
235 return "Domain name element too long";
236 rhn[j+1]=str[j];
237 }
238
239 if(j<=i) {
240 if(j<len && str[j])
241 return "Empty name element in domain name";
242 else
243 break;
244 }
245
246 rhn[i]=(unsigned char)(j-i);
247 i = j+1;
248 } while(j<len && str[j]);
249
250 rhn[i]=0;
251 if(i==0)
252 return "Empty domain name not allowed";
253 return NULL;
254 }
255
256 /*
257 * Take a host name as used in the dns transfer protocol (a length byte,
258 * followed by the first part of the name, ..., followed by a 0 length byte),
259 * and return a string in the usual dotted notation. Length checking is done
260 * elsewhere (in decompress_name), this takes names from the cache which are
261 * validated. No more than "size" bytes will be written to the buffer str points to
262 * (but size must be positive).
263 * If the labels in rhn contains non-printable characters, these are represented
264 * in the result by a backslash followed three octal digits. Additionally some
265 * special characters are preceded by a backslash. Normally the result should
266 * fit within DNSNAMEBUFSIZE bytes, but if there are many non-printable characters, the
267 * receiving buffer may not be able to contain the full result. In that case the
268 * truncation in the result is indicated by series of trailing dots. This
269 * function is only used for diagnostic purposes, thus a truncated result should
270 * not be a very serious problem (and should only occur under pathological
271 * circumstances).
272 */
rhn2str(const unsigned char * rhn,unsigned char * str,unsigned int size)273 const unsigned char *rhn2str(const unsigned char *rhn, unsigned char *str, unsigned int size)
274 {
275 unsigned int i,j,lb;
276
277 if(size==0) return NULL;
278
279 i=0; j=0;
280 lb=rhn[i++];
281 if (!lb) {
282 if(size>=2)
283 str[j++]='.';
284 }
285 else {
286 do {
287 for (;lb;--lb) {
288 unsigned char c;
289 if(j+2>=size)
290 goto overflow;
291 c=rhn[i++];
292 if(isgraph(c)) {
293 if(c=='.' || c=='\\' || c=='"') {
294 str[j++]='\\';
295 if(j+2>=size)
296 goto overflow;
297 }
298 str[j++]=c;
299 }
300 else {
301 unsigned int rem=size-1-j;
302 int n=snprintf(charp &str[j],rem,"\\%03o",c);
303 if(n<0 || n>=rem) {
304 str[j++]='.';
305 goto overflow;
306 }
307 j+=n;
308 }
309 }
310 str[j++]='.';
311 lb=rhn[i++];
312 } while(lb);
313 }
314 str[j]=0;
315 return str;
316
317 overflow:
318 j=size;
319 str[--j]=0;
320 if(j>0) {
321 str[--j]='.';
322 if(j>0) {
323 str[--j]='.';
324 if(j>0)
325 str[--j]='.';
326 }
327 }
328 return str;
329 }
330
331 /* Return the length of a domain name in transport format.
332 The definition has in fact been moved to helpers.h as an inline function.
333 Note added by Paul Rombouts:
334 Compared to the definition used by Thomas Moestl (strlen(rhn)+1), the following definition of rhnlen
335 may yield a different result in certain error situations (when a domain name segment contains null byte).
336 */
337 #if 0
338 unsigned int rhnlen(const unsigned char *rhn)
339 {
340 unsigned int i=0,lb;
341
342 while((lb=rhn[i++]))
343 i+=lb;
344 return i;
345 }
346 #endif
347
348 /*
349 * Non-validating rhn copy (use with checked or generated data only).
350 * Returns number of characters copied. The buffer dst points to is assumed to be DNSNAMEBUFSIZE (or
351 * at any rate large enough) bytes in size.
352 * The answer assembly code uses this; it is guaranteed to not clobber anything
353 * after the name.
354 */
rhncpy(unsigned char * dst,const unsigned char * src)355 unsigned int rhncpy(unsigned char *dst, const unsigned char *src)
356 {
357 unsigned int len = rhnlen(src);
358
359 PDNSD_ASSERT(len<=DNSNAMEBUFSIZE,"rhncpy: src too long!");
360 memcpy(dst,src,len>DNSNAMEBUFSIZE?DNSNAMEBUFSIZE:len);
361 return len;
362 }
363
364
365 /* Check whether a name is a normal wire-encoded domain name,
366 i.e. is not compressed, doesn't use extended labels and is not
367 too long.
368 */
isnormalencdomname(const unsigned char * rhn,unsigned maxlen)369 int isnormalencdomname(const unsigned char *rhn, unsigned maxlen)
370 {
371 unsigned int i,lb;
372
373 if(maxlen>DNSNAMEBUFSIZE)
374 maxlen=DNSNAMEBUFSIZE;
375 for(i=0;;) {
376 if(i>=maxlen) return 0;
377 lb=rhn[i++];
378 if(lb==0) break;
379 if(lb>0x3f) return 0;
380 i += lb;
381 }
382
383 return 1;
384 }
385
str2pdnsd_a(const char * addr,pdnsd_a * a)386 int str2pdnsd_a(const char *addr, pdnsd_a *a)
387 {
388 #ifdef ENABLE_IPV4
389 if (run_ipv4) {
390 return inet_aton(addr,&a->ipv4);
391 }
392 #endif
393 #ifdef ENABLE_IPV6
394 ELSE_IPV6 {
395 /* Try to map an IPv4 address to IPv6 */
396 struct in_addr a4;
397 if(inet_aton(addr,&a4)) {
398 a->ipv6=global.ipv4_6_prefix;
399 ((uint32_t *)(&a->ipv6))[3]=a4.s_addr;
400 return 1;
401 }
402 return inet_pton(AF_INET6,addr,&a->ipv6)>0;
403 }
404 #endif
405 /* return 0; */
406 }
407
408 /* definition moved to helpers.h */
409 #if 0
410 int is_inaddr_any(pdnsd_a *a)
411 {
412 return SEL_IPVER( a->ipv4.s_addr==INADDR_ANY,
413 IN6_IS_ADDR_UNSPECIFIED(&a->ipv6) );
414 }
415 #endif
416
417 /*
418 * This is used for user output only, so it does not matter when an error occurs.
419 */
pdnsd_a2str(pdnsd_a * a,char * buf,int maxlen)420 const char *pdnsd_a2str(pdnsd_a *a, char *buf, int maxlen)
421 {
422 const char *res= SEL_IPVER( inet_ntop(AF_INET,&a->ipv4,buf,maxlen),
423 inet_ntop(AF_INET6,&a->ipv6,buf,maxlen) );
424 if (!res) {
425 log_error("inet_ntop: %s", strerror(errno));
426 return "?.?.?.?";
427 }
428
429 return res;
430 }
431
432
433 /* Appropriately set our random device */
434 #ifdef R_DEFAULT
435 # if (TARGET == TARGET_BSD) && !defined(__NetBSD__)
436 # define R_ARC4RANDOM 1
437 # else
438 # define R_RANDOM 1
439 # endif
440 #endif
441
442 #ifdef RANDOM_DEVICE
443 FILE *rand_file;
444 #endif
445
446 #ifdef R_RANDOM
init_crandom()447 void init_crandom()
448 {
449 struct timeval tv;
450 struct timezone tz;
451
452 gettimeofday(&tv,&tz);
453 srandom(tv.tv_sec^tv.tv_usec); /* not as guessable as time() */
454 }
455 #endif
456
457 /* initialize the PRNG */
init_rng()458 int init_rng()
459 {
460 #ifdef RANDOM_DEVICE
461 if (!(rand_file=fopen(RANDOM_DEVICE,"r"))) {
462 log_error("Could not open %s.",RANDOM_DEVICE);
463 return 0;
464 }
465 #endif
466 #ifdef R_RANDOM
467 init_crandom();
468 #endif
469 return 1;
470 }
471
472 /* The following function is now actually defined as a macro in helpers.h */
473 #if 0
474 void free_rng()
475 {
476 #ifdef RANDOM_DEVICE
477 if (rand_file)
478 fclose(rand_file);
479 #endif
480 }
481 #endif
482
483 /* generate a (more or less) random number 16 bits long. */
get_rand16()484 unsigned short get_rand16()
485 {
486 #ifdef RANDOM_DEVICE
487 unsigned short rv;
488
489 if (rand_file) {
490 if (fread(&rv,sizeof(rv),1, rand_file)!=1) {
491 log_error("Error while reading from random device: %s", strerror(errno));
492 pdnsd_exit();
493 }
494 return rv&0xffff;
495 } else
496 return random()&0xffff;
497 #endif
498 #ifdef R_RANDOM
499 return random()&0xffff;
500 #endif
501 #ifdef R_ARC4RANDOM
502 return arc4random()&0xffff;
503 #endif
504 }
505
506 /* fsprintf does formatted output to a file descriptor.
507 The functionality is similar to fprintf, but note that fd
508 is of type int instead of FILE*.
509 This function has been rewritten by Paul Rombouts */
fsprintf(int fd,const char * format,...)510 int fsprintf(int fd, const char *format, ...)
511 {
512 int n;
513 va_list va;
514
515 {
516 char buf[256];
517
518 va_start(va,format);
519 n=vsnprintf(buf,sizeof(buf),format,va);
520 va_end(va);
521
522 if(n<(int)sizeof(buf)) {
523 if(n>0) n=write_all(fd,buf,n);
524 return n;
525 }
526 }
527 /* retry with a right sized buffer, needs glibc 2.1 or higher to work */
528 {
529 unsigned bufsize=n+1;
530 char buf[bufsize];
531
532 va_start(va,format);
533 n=vsnprintf(buf,bufsize,format,va);
534 va_end(va);
535
536 if(n>0) n=write_all(fd,buf,n);
537 }
538 return n;
539 }
540
541 /* Convert data into a hexadecimal representation (for debugging purposes)..
542 The result is stored in the character array buf.
543 If buf is not large enough to hold the result, the
544 truncation is indicated by trailing dots.
545 */
hexdump(const void * data,int dlen,char * buf,int buflen)546 void hexdump(const void *data, int dlen, char *buf, int buflen)
547 {
548 const unsigned char *p=data;
549 int i,j=0;
550 for(i=0;i<dlen;++i) {
551 int rem=buflen-j;
552 int n=snprintf(buf+j, rem, i==0?"%02x":" %02x", p[i]);
553 if(n<0 || n>=rem) goto truncated;
554 j += n;
555 }
556 goto done;
557
558 truncated:
559 if(j>=6) {
560 j -= 3;
561 if(j+4>=buflen)
562 j -= 3;
563 buf[j++]=' ';
564 buf[j++]='.';
565 buf[j++]='.';
566 buf[j++]='.';
567 }
568 else {
569 int ndots=buflen-1;
570 if(ndots>3) ndots=3;
571 j=0;
572 while(j<ndots) buf[j++]='.';
573 }
574 done:
575 buf[j]=0;
576 }
577
578 /* Copy string "in" to "str" buffer, converting non-printable characters
579 to escape sequences.
580 Returns the length of the output string, or -1 if the result does not
581 fit in the output buffer.
582 */
escapestr(const char * in,int ilen,char * str,int size)583 int escapestr(const char *in, int ilen, char *str, int size)
584 {
585 int i,j=0;
586 for(i=0;i<ilen;++i) {
587 unsigned char c;
588 if(j+1>=size)
589 return -1;
590 c=in[i];
591 if(!isprint(c)) {
592 int rem=size-j;
593 int n=snprintf(&str[j],rem,"\\%03o",c);
594 if(n<0 || n>=rem) {
595 return -1;
596 }
597 j+=n;
598 }
599 else {
600 if(c=='\\' || c=='"') {
601 str[j++]='\\';
602 if(j+1>=size)
603 return -1;
604 }
605 str[j++]=c;
606 }
607 }
608 str[j]=0;
609 return j;
610 }
611
612 /*
613 * This is not like strcmp, but will return 1 on match or 0 if the
614 * strings are different.
615 */
616 /* definition moved to helpers.h as an inline function. */
617 #if 0
618 int stricomp(char *a, char *b)
619 {
620 int i;
621 if (strlen(a) != strlen(b))
622 return 0;
623 for (i=0;i<strlen(a);i++) {
624 if (tolower(a[i])!=tolower(b[i]))
625 return 0;
626 }
627 return 1;
628 }
629 #endif
630
631 /* Bah. I want strlcpy */
632 /* definition moved to helpers.h */
633 #if 0
634 int strncp(char *dst, char *src, int dstsz)
635 {
636 char o;
637
638 strncpy(dst,src,dstsz);
639 o=dst[dstsz-1];
640 dst[dstsz-1]='\0';
641 if (strlen(dst) >= dstsz-1 && o!='\0')
642 return 0;
643 return 1;
644 }
645 #endif
646
647 #ifndef HAVE_GETLINE
648 /* Note by Paul Rombouts: I know that getline is a GNU extension and is not really portable,
649 but the alternative standard functions have some real problems.
650 The following substitute does not have exactly the same semantics as the GNU getline,
651 but it should be good enough, as long as the stream doesn't contain any null chars.
652 This version is actually based on fgets_realloc() that I found in the WWWOFFLE source.
653 */
654
655 #define BUFSIZE 256
getline(char ** lineptr,size_t * n,FILE * stream)656 int getline(char **lineptr, size_t *n, FILE *stream)
657 {
658 char *line=*lineptr;
659 size_t sz=*n,i;
660
661 if(!line || sz<BUFSIZE) {
662 sz=BUFSIZE;
663 line = realloc(line,sz);
664 if(!line) return -1;
665 *lineptr=line;
666 *n=sz;
667 }
668
669 for (i=0;;) {
670 char *lni;
671
672 if(!(lni=fgets(line+i,sz-i,stream))) {
673 if(i && feof(stream))
674 break;
675 else
676 return -1;
677 }
678
679 i += strlen(lni);
680
681 if(i<sz-1 || line[i-1]=='\n')
682 break;
683
684 sz += BUFSIZE;
685 line = realloc(line,sz);
686 if(!line) return -1;
687 *lineptr=line;
688 *n=sz;
689 }
690
691 return i;
692 }
693 #undef BUFSIZE
694 #endif
695
696
697 #ifndef HAVE_VASPRINTF
698 /* On systems where the macro va_copy is not available, hopefully simple assignment will work.
699 Otherwise, I don't see any portable way of defining vasprintf() (without using a completely
700 different algorithm).
701 */
702 #ifndef va_copy
703 # ifdef __va_copy
704 # define va_copy __va_copy
705 # else
706 # warning "No va_copy or __va_copy macro found, trying simple assignment."
707 # define va_copy(dst,src) ((dst)=(src))
708 # endif
709 #endif
710
vasprintf(char ** lineptr,const char * format,va_list va)711 int vasprintf (char **lineptr, const char *format, va_list va)
712 {
713 int sz=128,n;
714 char *line=malloc(sz);
715 va_list vasave;
716
717 if(!line) return -1;
718
719 va_copy(vasave,va);
720 n=vsnprintf(line,sz,format,va);
721
722 if(n>=sz) {
723 /* retry with a right sized buffer, needs glibc 2.1 or higher to work */
724 sz=n+1;
725 {
726 char *tmp=realloc(line,sz);
727 if(tmp) {
728 line=tmp;
729 n=vsnprintf(line,sz,format,vasave);
730 }
731 else
732 n= -1;
733 }
734 }
735 va_end(vasave);
736
737 if(n>=0)
738 *lineptr=line;
739 else
740 free(line);
741 return n;
742 }
743 #endif
744
745 #ifndef HAVE_ASPRINTF
asprintf(char ** lineptr,const char * format,...)746 int asprintf (char **lineptr, const char *format, ...)
747 {
748 int n;
749 va_list va;
750
751 va_start(va,format);
752 n=vasprintf(lineptr,format,va);
753 va_end(va);
754
755 return n;
756 }
757 #endif
758
759 #ifndef HAVE_INET_NTOP
inet_ntop(int af,const void * src,char * dst,size_t size)760 const char *inet_ntop(int af, const void *src, char *dst, size_t size)
761 {
762 const char *rc = NULL;
763
764 if (src != NULL && dst != NULL && size > 0) {
765 switch (af) {
766 case AF_INET:
767 {
768 const unsigned char *p=src;
769 int n = snprintf(dst, size, "%u.%u.%u.%u",
770 p[0],p[1],p[2],p[3]);
771 if (n >= 0 && n < size) rc = dst;
772 }
773 break;
774
775 #ifdef AF_INET6
776 case AF_INET6:
777 {
778 const unsigned char *p=src;
779 unsigned int i,offs=0;
780 for (i=0;i<16;i+=2) {
781 int n=snprintf(dst+offs, size-offs,i==0?"%x":":%x", ((unsigned)p[i]<<8)|p[i+1]);
782 if(n<0) return NULL;
783 offs+=n;
784 if(offs>=size) return NULL;
785 }
786 rc = dst;
787 }
788 break;
789 #endif
790 }
791 }
792
793 return rc;
794 }
795 #endif
796