/[svn]/hopm/trunk/src/firedns.c
ViewVC logotype

Contents of /hopm/trunk/src/firedns.c

Parent Directory Parent Directory | Revision Log Revision Log


Revision 5072 - (show annotations)
Mon Dec 22 15:33:29 2014 UTC (5 years, 3 months ago) by michael
File MIME type: text/x-chdr
File size: 19132 byte(s)
- Fixed a bunch of compile warnings

1 /* vim: set shiftwidth=3 softtabstop=3 expandtab: */
2
3 /*
4 firedns.c - firedns library
5 Copyright (C) 2002 Ian Gulliver
6
7 This file has been gutted and mucked with for use in BOPM - see the
8 real library at http://ares.penguinhosting.net/~ian/ before you judge
9 firedns based on this..
10
11 This program is free software; you can redistribute it and/or modify
12 it under the terms of version 2 of the GNU General Public License as
13 published by the Free Software Foundation.
14
15 This program is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 GNU General Public License for more details.
19
20 You should have received a copy of the GNU General Public License
21 along with this program; if not, write to the Free Software
22 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23 */
24
25 #include "setup.h"
26
27 #include <stdlib.h>
28 #include <time.h>
29 #include <sys/types.h>
30 #include <sys/socket.h>
31 #include <sys/poll.h>
32 #include <sys/time.h>
33 #include <netinet/in.h>
34 #include <string.h>
35 #include <unistd.h>
36 #include <stdio.h>
37 #include <errno.h>
38 #include <fcntl.h>
39
40 #include "compat.h"
41 #include "inet.h"
42 #include "malloc.h"
43 #include "firedns.h"
44 #include "config.h"
45 #include "list.h"
46 #include "log.h"
47 #include "dnsbl.h"
48
49 RCSID("$Id: firedns.c,v 1.22 2005/06/03 12:58:12 dg Exp $");
50
51 #define FIREDNS_TRIES 3
52 #define min(a,b) (a < b ? a : b)
53
54 /* Global variables */
55
56 int fdns_errno = FDNS_ERR_NONE;
57 unsigned int fdns_fdinuse = 0;
58
59 /* Variables local to this file */
60
61 /* up to FDNS_MAX nameservers; populated by firedns_init() */
62 static struct in_addr servers4[FDNS_MAX];
63 /* actual count of nameservers; set by firedns_init() */
64 static int i4;
65
66 #ifdef IPV6
67 static int i6;
68 static struct in6_addr servers6[FDNS_MAX];
69 #endif
70
71 /*
72 * linked list of open DNS queries; populated by firedns_add_query(),
73 * decimated by firedns_getresult()
74 */
75 static list_t *CONNECTIONS = NULL;
76
77 /*
78 * List of errors, in order of values used in FDNS_ERR_*, returned by
79 * firedns_strerror
80 */
81 static const char *errors[] = {
82 "Success",
83 "Format error",
84 "Server failure",
85 "Name error",
86 "Not implemented",
87 "Refused",
88 "Timeout",
89 "Network error",
90 "FD Limit reached",
91 "Unknown error"
92 };
93
94 /* Structures */
95
96 /* open DNS query */
97 struct s_connection
98 {
99 /*
100 * unique ID (random number), matches header ID; both set by
101 * firedns_add_query()
102 */
103 unsigned char id[2];
104 unsigned short class;
105 unsigned short type;
106 /* file descriptor returned from sockets */
107 int fd;
108 void *info;
109 time_t start;
110 char lookup[256];
111 #ifdef IPV6
112 int v6;
113 #endif
114 };
115
116 struct s_rr_middle
117 {
118 unsigned short type;
119 unsigned short class;
120 /* XXX - firedns depends on this being 4 bytes */
121 uint32 ttl;
122 unsigned short rdlength;
123 };
124
125 /* DNS query header */
126 struct s_header
127 {
128 unsigned char id[2];
129 unsigned char flags1;
130 #define FLAGS1_MASK_QR 0x80
131 /* bitshift right 3 */
132 #define FLAGS1_MASK_OPCODE 0x78
133 #define FLAGS1_MASK_AA 0x04
134 #define FLAGS1_MASK_TC 0x02
135 #define FLAGS1_MASK_RD 0x01
136
137 unsigned char flags2;
138 #define FLAGS2_MASK_RA 0x80
139 #define FLAGS2_MASK_Z 0x70
140 #define FLAGS2_MASK_RCODE 0x0f
141
142 unsigned short qdcount;
143 unsigned short ancount;
144 unsigned short nscount;
145 unsigned short arcount;
146 /* DNS question, populated by firedns_build_query_payload() */
147 unsigned char payload[512];
148 };
149
150 /* Function prototypes */
151
152 static struct s_connection *firedns_add_query(void);
153 static int firedns_doquery(struct s_connection *s);
154 static int firedns_build_query_payload(const char * const name,
155 unsigned short rr, unsigned short class, unsigned char * payload);
156 static int firedns_send_requests(struct s_header *h, struct s_connection *s,
157 int l);
158
159
160 void firedns_init(void)
161 {
162 /*
163 * populates servers4 (or -6) struct with up to FDNS_MAX nameserver IP
164 * addresses from /etc/firedns.conf (or /etc/resolv.conf)
165 */
166 FILE *f;
167 int i;
168 struct in_addr addr4;
169 char buf[1024];
170 const char *file;
171 #ifdef IPV6
172
173 struct in6_addr addr6;
174
175 i6 = 0;
176 #endif
177
178 i4 = 0;
179
180 /* Initialize connections list */
181 CONNECTIONS = list_create();
182
183 srand((unsigned int) time(NULL));
184 memset(servers4,'\0',sizeof(struct in_addr) * FDNS_MAX);
185 #ifdef IPV6
186
187 memset(servers6,'\0',sizeof(struct in6_addr) * FDNS_MAX);
188 #endif
189 /* read etc/firedns.conf if we've got it, otherwise parse /etc/resolv.conf */
190 f = fopen(FDNS_CONFIG_PREF,"r");
191 if (f == NULL)
192 {
193 f = fopen(FDNS_CONFIG_FBCK,"r");
194 if (f == NULL)
195 {
196 log_printf("Unable to open %s", FDNS_CONFIG_FBCK);
197 return;
198 }
199 file = FDNS_CONFIG_FBCK;
200 while (fgets(buf,1024,f) != NULL)
201 {
202 if (strncmp(buf,"nameserver",10) == 0)
203 {
204 i = 10;
205 while (buf[i] == ' ' || buf[i] == '\t')
206 i++;
207 #ifdef IPV6
208 /* glibc /etc/resolv.conf seems to allow ipv6 server names */
209 if (i6 < FDNS_MAX)
210 {
211 if (inet_pton6(&buf[i], (char *)&addr6) != NULL)
212 {
213 memcpy(&servers6[i6++],&addr6,sizeof(struct in6_addr));
214 continue;
215 }
216 }
217 #endif
218 if (i4 < FDNS_MAX)
219 {
220 if (inet_aton(&buf[i], &addr4))
221 {
222 memcpy(&servers4[i4++],&addr4,sizeof(struct in_addr));
223 }
224 }
225 }
226 }
227 }
228 else
229 {
230 file = FDNS_CONFIG_PREF;
231 while (fgets(buf,1024,f) != NULL)
232 {
233 buf[strspn(buf, "0123456789.")] = '\0';
234 #ifdef IPV6
235 if (i6 < FDNS_MAX)
236 {
237 if (inet_pton(AF_INET6, buf, (char *)&addr6))
238 {
239 memcpy(&servers6[i6++], &addr6, sizeof(struct in6_addr));
240 continue;
241 }
242 }
243 #endif
244 if (i4 < FDNS_MAX)
245 {
246 if (inet_pton(AF_INET, buf, (char *)&addr4))
247 memcpy(&servers4[i4++],&addr4,sizeof(struct in_addr));
248 }
249 }
250 }
251 fclose(f);
252
253 if(i4 == 0
254 #ifdef IPV6 /* (yuck) */
255 && i6
256 #endif
257 )
258 {
259 log_printf("FIREDNS -> No nameservers found in %s", file);
260 exit(EXIT_FAILURE);
261 }
262 }
263
264 struct in_addr *firedns_resolveip4(const char * const name)
265 { /* immediate A query */
266 static struct in_addr addr;
267
268 if(inet_aton(name, &addr))
269 return &addr;
270
271 return (struct in_addr *) firedns_resolveip(FDNS_QRY_A, name);
272 }
273
274 struct in6_addr *firedns_resolveip6(const char * const name)
275 { /* immediate AAAA query */
276 return (struct in6_addr *) firedns_resolveip(FDNS_QRY_AAAA, name);
277 }
278
279 char *firedns_resolveip(int type, const char * const name)
280 { /* resolve a query of a given type */
281 int fd, t;
282 struct firedns_result *result;
283 struct timeval tv;
284 fd_set s;
285
286 for (t = 0; t < FIREDNS_TRIES; t++)
287 {
288 fd = firedns_getip(type, name, NULL);
289 if (fd == -1)
290 return NULL;
291
292 tv.tv_sec = 5;
293 tv.tv_usec = 0;
294 FD_ZERO(&s);
295 FD_SET(fd, &s);
296 select(fd + 1, &s, NULL, NULL, &tv);
297
298 result = firedns_getresult(fd);
299
300 if (fdns_errno == FDNS_ERR_NONE)
301 /* Return is from static memory in getresult, so there is no need to
302 copy it until the next call to firedns. */
303 return result->text;
304 else if(fdns_errno == FDNS_ERR_NXDOMAIN)
305 return NULL;
306 }
307 if(fdns_errno == FDNS_ERR_NONE)
308 fdns_errno = FDNS_ERR_TIMEOUT;
309 return NULL;
310 }
311
312 /*
313 * build, add and send specified query; retrieve result with
314 * firedns_getresult()
315 */
316 int firedns_getip(int type, const char * const name, void *info)
317 {
318 struct s_connection *s;
319 node_t *node;
320 int fd;
321
322 s = firedns_add_query();
323
324 s->class = 1;
325 s->type = type;
326 strncpy(s->lookup, name, 256);
327 s->info = info;
328
329 if(fdns_fdinuse >= OptionsItem->dns_fdlimit)
330 {
331 fdns_errno = FDNS_ERR_FDLIMIT;
332 /* Don't add to queue if there is no info */
333 if(info == NULL)
334 {
335 MyFree(s);
336 }else{
337 node = node_create(s);
338 list_add(CONNECTIONS, node);
339 }
340 return -1;
341 }
342
343 fd = firedns_doquery(s);
344
345 if(fd == -1)
346 {
347 MyFree(s);
348 return -1;
349 }
350
351 node = node_create(s);
352 list_add(CONNECTIONS, node);
353 return fd;
354 }
355
356 static struct s_connection *firedns_add_query(void)
357 { /* build DNS query, add to list */
358 struct s_connection *s;
359
360 /* create new connection object */
361 s = MyMalloc(sizeof *s);
362
363 /* verified by firedns_getresult() */
364 s->id[0] = rand() % 255;
365 s->id[1] = rand() % 255;
366
367 s->fd = -1;
368
369 return s;
370 }
371
372 static int firedns_doquery(struct s_connection *s)
373 {
374 int len;
375 struct s_header h;
376
377 len = firedns_build_query_payload(s->lookup, s->type, 1,
378 (unsigned char *)&h.payload);
379
380 if(len == -1)
381 {
382 fdns_errno = FDNS_ERR_FORMAT;
383 return -1;
384 }
385
386 return firedns_send_requests(&h, s, len);
387 }
388
389 /*
390 * populate payload with query: name= question, rr= record type
391 */
392 static int firedns_build_query_payload(const char * const name,
393 unsigned short rr, unsigned short class, unsigned char * payload)
394 {
395 short payloadpos;
396 const char * tempchr, * tempchr2;
397 unsigned short l;
398
399 payloadpos = 0;
400 tempchr2 = name;
401
402 /* split name up into labels, create query */
403 while ((tempchr = strchr(tempchr2,'.')) != NULL)
404 {
405 l = tempchr - tempchr2;
406 if (payloadpos + l + 1 > 507)
407 return -1;
408 payload[payloadpos++] = l;
409 memcpy(&payload[payloadpos],tempchr2,l);
410 payloadpos += l;
411 tempchr2 = &tempchr[1];
412 }
413 l = strlen(tempchr2);
414 if (l)
415 {
416 if (payloadpos + l + 2 > 507)
417 return -1;
418 payload[payloadpos++] = l;
419 memcpy(&payload[payloadpos],tempchr2,l);
420 payloadpos += l;
421 payload[payloadpos++] = '\0';
422 }
423 if (payloadpos > 508)
424 return -1;
425 l = htons(rr);
426 memcpy(&payload[payloadpos],&l,2);
427 l = htons(class);
428 memcpy(&payload[payloadpos + 2],&l,2);
429 return payloadpos + 4;
430 }
431
432 /* send DNS query */
433 static int firedns_send_requests(struct s_header *h, struct s_connection *s,
434 int l)
435 {
436 int i, sent_ok = 0;
437 struct sockaddr_in addr4;
438
439 #ifdef IPV6
440 struct sockaddr_in6 addr6;
441 #endif
442
443 /* set header flags */
444 h->flags1 = 0 | FLAGS1_MASK_RD;
445 h->flags2 = 0;
446 h->qdcount = htons(1);
447 h->ancount = htons(0);
448 h->nscount = htons(0);
449 h->arcount = htons(0);
450 memcpy(h->id, s->id, 2);
451
452 /* try to create ipv6 or ipv4 socket */
453 #ifdef IPV6
454
455 s->v6 = 0;
456 if (i6 > 0)
457 {
458 s->fd = socket(PF_INET6, SOCK_DGRAM, 0);
459 if (s->fd != -1)
460 {
461 if (fcntl(s->fd, F_SETFL, O_NONBLOCK) != 0)
462 {
463 close(s->fd);
464 s->fd = -1;
465 }
466 }
467 if (s->fd != -1)
468 {
469 struct sockaddr_in6 addr6;
470 memset(&addr6,0,sizeof(addr6));
471 addr6.sin6_family = AF_INET6;
472 if (bind(s->fd,(struct sockaddr *)&addr6,sizeof(addr6)) == 0)
473 s->v6 = 1;
474 else
475 {
476 close(s->fd);
477 }
478 }
479 }
480 if (s->v6 == 0)
481 {
482 #endif
483 s->fd = socket(PF_INET, SOCK_DGRAM, 0);
484 if (s->fd != -1)
485 {
486 if (fcntl(s->fd, F_SETFL, O_NONBLOCK) != 0)
487 {
488 close(s->fd);
489 s->fd = -1;
490 }
491 }
492 if (s->fd != -1)
493 {
494 struct sockaddr_in addr;
495 memset(&addr,0,sizeof(addr));
496 addr.sin_family = AF_INET;
497 addr.sin_port = 0;
498 addr.sin_addr.s_addr = INADDR_ANY;
499 if (bind(s->fd,(struct sockaddr *)&addr,sizeof(addr)) != 0)
500 {
501 close(s->fd);
502 s->fd = -1;
503 }
504 }
505 if (s->fd == -1)
506 {
507 fdns_errno = FDNS_ERR_NETWORK;
508 return -1;
509 }
510 #ifdef IPV6
511
512 }
513 #endif
514
515
516 #ifdef IPV6
517 /* if we've got ipv6 support, an ip v6 socket, and ipv6 servers, send to them */
518 if (i6 > 0 && s->v6 == 1)
519 {
520 for (i = 0; i < i6; i++)
521 {
522 memset(&addr6,0,sizeof(addr6));
523 memcpy(&addr6.sin6_addr,&servers6[i],sizeof(addr6.sin6_addr));
524 addr6.sin6_family = AF_INET6;
525 addr6.sin6_port = htons(FDNS_PORT);
526 if(sendto(s->fd, h, l + 12, 0, (struct sockaddr *) &addr6, sizeof(addr6)) > 0)
527 sent_ok = 1;
528 }
529 }
530 #endif
531
532 for (i = 0; i < i4; i++)
533 {
534 #ifdef IPV6
535 /* send via ipv4-over-ipv6 if we've got an ipv6 socket */
536 if (s->v6 == 1)
537 {
538 memset(&addr6,0,sizeof(addr6));
539 memcpy(addr6.sin6_addr.s6_addr,"\0\0\0\0\0\0\0\0\0\0\xff\xff",12);
540 memcpy(&addr6.sin6_addr.s6_addr[12],&servers4[i].s_addr,4);
541 addr6.sin6_family = AF_INET6;
542 addr6.sin6_port = htons(FDNS_PORT);
543 if(sendto(s->fd, h, l + 12, 0, (struct sockaddr *) &addr6, sizeof(addr6)) > 0)
544 sent_ok = 1;
545 continue;
546 }
547 #endif
548 /* otherwise send via standard ipv4 boringness */
549 memset(&addr4,0,sizeof(addr4));
550 memcpy(&addr4.sin_addr,&servers4[i],sizeof(addr4.sin_addr));
551 addr4.sin_family = AF_INET;
552 addr4.sin_port = htons(FDNS_PORT);
553 if(sendto(s->fd, h, l + 12, 0, (struct sockaddr *) &addr4, sizeof(addr4)) > 0)
554 sent_ok = 1;
555 }
556
557 if(!sent_ok)
558 {
559 close(s->fd);
560 s->fd = -1;
561 fdns_errno = FDNS_ERR_NETWORK;
562 return -1;
563 }
564
565 time(&s->start);
566 fdns_fdinuse++;
567 fdns_errno = FDNS_ERR_NONE;
568 return s->fd;
569 }
570
571 struct firedns_result *firedns_getresult(const int fd)
572 { /* retrieve result of DNS query */
573 static struct firedns_result result;
574 struct s_header h;
575 struct s_connection *c;
576 node_t *node;
577 int l,i,q,curanswer;
578 struct s_rr_middle *rr, rrbacking;
579 char *src, *dst;
580 int bytes;
581
582 fdns_errno = FDNS_ERR_OTHER;
583 result.info = (void *) NULL;
584 memset(result.text, 0, sizeof(result.text));
585
586 /* Find query in list of dns lookups */
587 LIST_FOREACH(node, CONNECTIONS->head)
588 {
589 c = (struct s_connection *) node->data;
590 if(c->fd == fd)
591 break;
592 else
593 c = NULL;
594 }
595
596 /* query not found */
597 if(c == NULL)
598 return &result;
599
600 /* query found -- we remove in cleanup */
601
602 l = recv(c->fd,&h,sizeof(struct s_header),0);
603 result.info = (void *) c->info;
604 strncpy(result.lookup, c->lookup, 256);
605
606 if(l == -1)
607 {
608 fdns_errno = FDNS_ERR_NETWORK;
609 goto cleanup;
610 }
611
612 if (l < 12)
613 goto cleanup;
614 if (c->id[0] != h.id[0] || c->id[1] != h.id[1])
615 /* ID mismatch: we keep the connection, as this could be an answer to
616 a previous lookup.. */
617 return NULL;
618 if ((h.flags1 & FLAGS1_MASK_QR) == 0)
619 goto cleanup;
620 if ((h.flags1 & FLAGS1_MASK_OPCODE) != 0)
621 goto cleanup;
622 if ((h.flags2 & FLAGS2_MASK_RCODE) != 0)
623 {
624 fdns_errno = (h.flags2 & FLAGS2_MASK_RCODE);
625 goto cleanup;
626 }
627 h.ancount = ntohs(h.ancount);
628 if (h.ancount < 1) {
629 fdns_errno = FDNS_ERR_NXDOMAIN;
630 /* no sense going on if we don't have any answers */
631 goto cleanup;
632 }
633 /* skip queries */
634 i = 0;
635 q = 0;
636 l -= 12;
637 h.qdcount = ntohs(h.qdcount);
638 while (q < h.qdcount && i < l)
639 {
640 if (h.payload[i] > 63)
641 { /* pointer */
642 i += 6; /* skip pointer, class and type */
643 q++;
644 }
645 else
646 { /* label */
647 if (h.payload[i] == 0)
648 {
649 q++;
650 i += 5; /* skip nil, class and type */
651 }
652 else
653 i += h.payload[i] + 1; /* skip length and label */
654 }
655 }
656 /* &h.payload[i] should now be the start of the first response */
657 curanswer = 0;
658 while (curanswer < h.ancount)
659 {
660 q = 0;
661 while (q == 0 && i < l)
662 {
663 if (h.payload[i] > 63)
664 { /* pointer */
665 i += 2; /* skip pointer */
666 q = 1;
667 }
668 else
669 { /* label */
670 if (h.payload[i] == 0)
671 {
672 i++;
673 q = 1;
674 }
675 else
676 i += h.payload[i] + 1; /* skip length and label */
677 }
678 }
679 if (l - i < 10)
680 goto cleanup;
681 rr = (struct s_rr_middle *)&h.payload[i];
682 src = (char *) rr;
683 dst = (char *) &rrbacking;
684 for (bytes = sizeof(rrbacking); bytes; bytes--)
685 *dst++ = *src++;
686 rr = &rrbacking;
687 i += 10;
688 rr->rdlength = ntohs(rr->rdlength);
689 if (ntohs(rr->type) != c->type)
690 {
691 curanswer++;
692 i += rr->rdlength;
693 continue;
694 }
695 if (ntohs(rr->class) != c->class)
696 {
697 curanswer++;
698 i += rr->rdlength;
699 continue;
700 }
701 break;
702 }
703
704 if (curanswer == h.ancount)
705 goto cleanup;
706 if (i + rr->rdlength > l)
707 goto cleanup;
708 if (rr->rdlength > 1023)
709 goto cleanup;
710
711 fdns_errno = FDNS_ERR_NONE;
712 memcpy(result.text,&h.payload[i],rr->rdlength);
713 result.text[rr->rdlength] = '\0';
714
715 /* Clean-up */
716 cleanup:
717 list_remove(CONNECTIONS, node);
718 node_free(node);
719 close(c->fd);
720 fdns_fdinuse--;
721 MyFree(c);
722
723 return &result;
724 }
725
726 void firedns_cycle(void)
727 {
728 node_t *node, *next;
729 struct s_connection *p;
730 struct firedns_result *res, new_result;
731 static struct pollfd *ufds = NULL;
732 int fd;
733 unsigned int size, i;
734 time_t timenow;
735
736 if(LIST_SIZE(CONNECTIONS) == 0)
737 return;
738
739 if(ufds == NULL)
740 ufds = MyMalloc((sizeof *ufds) * OptionsItem->dns_fdlimit);
741
742 time(&timenow);
743 size = 0;
744
745 LIST_FOREACH_SAFE(node, next, CONNECTIONS->head)
746 {
747 if(size >= OptionsItem->dns_fdlimit)
748 break;
749
750 p = (struct s_connection *) node->data;
751
752 if(p->fd < 0)
753 continue;
754
755 if(p->fd > 0 && (p->start + FDNS_TIMEOUT) < timenow)
756 {
757 /* Timed out - remove from list */
758 list_remove(CONNECTIONS, node);
759 node_free(node);
760
761 memset(new_result.text, 0, sizeof(new_result.text));
762 new_result.info = p->info;
763 strncpy(new_result.lookup, p->lookup, 256);
764
765 close(p->fd);
766 fdns_fdinuse--;
767 MyFree(p);
768
769 fdns_errno = FDNS_ERR_TIMEOUT;
770
771 if(new_result.info != NULL)
772 dnsbl_result(&new_result);
773
774 continue;
775 }
776
777 ufds[size].events = 0;
778 ufds[size].revents = 0;
779 ufds[size].fd = p->fd;
780 ufds[size].events = POLLIN;
781
782 size++;
783 }
784
785
786 switch(poll(ufds, size, 0))
787 {
788 case -1:
789 case 0:
790 return;
791 }
792
793 LIST_FOREACH_SAFE(node, next, CONNECTIONS->head)
794 {
795 p = (struct s_connection *) node->data;
796 if(p->fd > 0)
797 {
798 for(i = 0; i < size; i++)
799 {
800 if((ufds[i].revents & POLLIN) && ufds[i].fd == p->fd)
801 {
802 fd = p->fd;
803 res = firedns_getresult(fd);
804
805 if(res != NULL && res->info != NULL)
806 dnsbl_result(res);
807 break;
808 }
809 }
810 }
811 else if(fdns_fdinuse < OptionsItem->dns_fdlimit)
812 {
813 firedns_doquery(p);
814 }
815 }
816 }
817
818 const char *firedns_strerror(int error)
819 {
820 if(error == FDNS_ERR_NETWORK)
821 return strerror(errno);
822 return errors[error];
823 }
824

svnadmin@ircd-hybrid.org
ViewVC Help
Powered by ViewVC 1.1.26