root/1.8.3/tags/p6/src/access.c

Revision 1167, 20.9 KB (checked in by shawnw, 13 months ago)

Merge devel into trunk for p6 release

Line 
1/**
2 * \file
3 *
4 * \brief Access control lists for PennMUSH.
5 * \verbatim
6 *
7 * The file access.cnf in the game directory will control all
8 * access-related directives, replacing lockout.cnf and sites.cnf
9 *
10 * The format of entries in the file will be:
11 *
12 * wild-host-name    [!]option [!]option [!]option ... # comment
13 *
14 * A wild-host-name is a wildcard pattern to match hostnames with.
15 * The wildcard "*" will work like UNIX filename globbing, so
16 * *.edu will match all sites with names ending in .edu, and
17 * *.*.*.*.* will match all sites with 4 periods in their name.
18 * 128.32.*.* will match all sites starting with 128.32 (UC Berkeley).
19 * You can also use user@host to match specific users if you know that
20 * the host is running ident and you trust its responses (nontrivial).
21 *
22 * The options that can be specified are:
23 * *CONNECT              Allow connections to non-guest players
24 * *GUEST                Allow connection to guests
25 * *CREATE               Allow player creation at login screen
26 * DEFAULT               All of the above
27 * NONE                 None of the above
28 * SUSPECT              Set all players connecting from the site suspect
29 * REGISTER             Allow players to use the "register" connect command
30 * DENY_SILENT          Don't log when someone's denied access from here
31 * REGEXP               Treat the hostname pattern as a regular expression
32 * *GOD                  God can connect from this pattern.
33 * *WIZARD               Wizards can connect from this pattern.
34 * *ADMIN                Admins can connect from this pattern.
35 *
36 * Options that are *'d can be prefaced by a !, meaning "Don't allow".
37 *
38 * The file is parsed line-by-line in order. This makes it possible
39 * to explicitly allow only certain sites to connect and deny all others,
40 * or vice versa. Sites can only do the options that are specified
41 * in the first line they match.
42 *
43 * If a site is listed in the file with no options at all, it is
44 * disallowed from any access (treated as !CONNECT, basically)
45 *
46 * If a site doesn't match any line in the file, it is allowed any
47 * toggleable access (treated as DEFAULT) but isn't SUSPECT or REGISTER.
48 *
49 * "make access" produces access.cnf from lockout.cnf/sites.cnf
50 *
51 * @sitelock'd sites appear after the line "@sitelock" in the file
52 * Using @sitelock writes out the file.
53 *
54 * \endverbatim
55 */
56
57#include "config.h"
58#include "copyrite.h"
59#include <stdio.h>
60#include <stdlib.h>
61#include <string.h>
62#include <ctype.h>
63#ifdef I_SYS_TYPES
64#include <sys/types.h>
65#endif
66#include <fcntl.h>
67#ifdef I_SYS_TIME
68#include <sys/time.h>
69#ifdef TIME_WITH_SYS_TIME
70#include <time.h>
71#endif
72#else
73#include <time.h>
74#endif
75#ifdef I_UNISTD
76#include <unistd.h>
77#endif
78#include "conf.h"
79#include "externs.h"
80#include "mypcre.h"
81#include "access.h"
82#include "mymalloc.h"
83#include "match.h"
84#include "parse.h"
85#include "log.h"
86#include "mushdb.h"
87#include "dbdefs.h"
88#include "flags.h"
89#include "confmagic.h"
90
91/** An access flag. */
92typedef struct a_acsflag acsflag;
93/** An access flag.
94 * This structure is used to build a table of access control flags.
95 */
96struct a_acsflag {
97  const char *name;             /**< Name of the access flag */
98  bool toggle;                   /**< Is this a negatable flag? */
99  uint32_t flag;                     /**< Bitmask of the flag */
100};
101static acsflag acslist[] = {
102  {"connect", 1, ACS_CONNECT},
103  {"create", 1, ACS_CREATE},
104  {"guest", 1, ACS_GUEST},
105  {"default", 0, ACS_DEFAULT},
106  {"register", 0, ACS_REGISTER},
107  {"suspect", 0, ACS_SUSPECT},
108  {"deny_silent", 0, ACS_DENY_SILENT},
109  {"regexp", 0, ACS_REGEXP},
110  {"god", 1, ACS_GOD},
111  {"wizard", 1, ACS_WIZARD},
112  {"admin", 1, ACS_ADMIN},
113  {NULL, 0, 0}
114};
115
116static struct access *access_top;
117static void free_access_list(void);
118
119extern const unsigned char *tables;
120
121static struct access *
122sitelock_alloc(const char *host, dbref who,
123               uint32_t can, uint32_t cant,
124               const char *comment, const char **errptr)
125  __attribute_malloc__;
126
127    static struct access *sitelock_alloc(const char *host, dbref who,
128                                         uint32_t can, uint32_t cant,
129                                         const char *comment,
130                                         const char **errptr)
131{
132  struct access *tmp;
133  tmp = mush_malloc(sizeof(struct access), "sitelock.rule");
134  if (!tmp) {
135    static const char *memerr = "unable to allocate memory";
136    if (errptr)
137      *errptr = memerr;
138    return NULL;
139  }
140  tmp->who = who;
141  tmp->can = can;
142  tmp->cant = cant;
143  mush_strncpy(tmp->host, host, BUFFER_LEN);
144  if (comment)
145    mush_strncpy(tmp->comment, comment, BUFFER_LEN);
146  else
147    tmp->comment[0] = '\0';
148  tmp->next = NULL;
149
150  if (can & ACS_REGEXP) {
151    int erroffset = 0;
152    tmp->re = pcre_compile(host, 0, errptr, &erroffset, tables);
153    if (!tmp->re) {
154      mush_free(tmp, "sitelock.rule");
155      return NULL;
156    }
157  } else
158    tmp->re = NULL;
159
160  return tmp;
161}
162
163static bool
164add_access_node(const char *host, dbref who, uint32_t can,
165                uint32_t cant, const char *comment, const char **errptr)
166{
167  struct access *end, *tmp;
168
169  tmp = sitelock_alloc(host, who, can, cant, comment, errptr);
170  if (!tmp)
171    return false;
172
173  if (!access_top) {
174    /* Add to the beginning */
175    access_top = tmp;
176  } else {
177    end = access_top;
178    while (end->next)
179      end = end->next;
180    end->next = tmp;
181  }
182  return true;
183}
184
185
186/** Read the access.cnf file.
187 * Initialize the access rules linked list and read in the access.cnf file.
188 * \return true if successful, false if not
189 */
190bool
191read_access_file(void)
192{
193  FILE *fp;
194  char buf[BUFFER_LEN];
195  char *p;
196  uint32_t can, cant;
197  int retval;
198  dbref who;
199  char *comment;
200  const char *errptr = NULL;
201
202  if (access_top) {
203    /* We're reloading the file, so we've got to delete any current
204     * entries
205     */
206    free_access_list();
207  }
208  access_top = NULL;
209  /* Be sure we have a file descriptor */
210  release_fd();
211  fp = fopen(ACCESS_FILE, FOPEN_READ);
212  if (!fp) {
213    do_rawlog(LT_ERR, T("Access file %s not found."), ACCESS_FILE);
214    retval = 0;
215  } else {
216    do_rawlog(LT_ERR, "Reading %s", ACCESS_FILE);
217    while (fgets(buf, BUFFER_LEN, fp)) {
218      /* Strip end of line if it's \r\n or \n */
219      if ((p = strchr(buf, '\r')))
220        *p = '\0';
221      else if ((p = strchr(buf, '\n')))
222        *p = '\0';
223      /* Find beginning of line; ignore blank lines */
224      p = buf;
225      if (*p && isspace((unsigned char) *p))
226        p++;
227      if (*p && *p != '#') {
228        can = cant = 0;
229        comment = NULL;
230        /* Is this the @sitelock entry? */
231        if (!strncasecmp(p, "@sitelock", 9)) {
232          if (!add_access_node("@sitelock", AMBIGUOUS, ACS_SITELOCK, 0, "",
233                               &errptr))
234            do_log(LT_ERR, GOD, GOD, T("Failed to add sitelock node: %s"),
235                   errptr);
236        } else {
237          if ((comment = strchr(p, '#'))) {
238            *comment++ = '\0';
239            while (*comment && isspace((unsigned char) *comment))
240              comment++;
241          }
242          /* Move past the host name */
243          while (*p && !isspace((unsigned char) *p))
244            p++;
245          if (*p)
246            *p++ = '\0';
247          if (!parse_access_options(p, &who, &can, &cant, NOTHING))
248            /* Nothing listed, so assume we can't do anything! */
249            cant = ACS_DEFAULT;
250          if (!add_access_node(buf, who, can, cant, comment, &errptr))
251            do_log(LT_ERR, GOD, GOD, T("Failed to add access node: %s"),
252                   errptr);
253        }
254      }
255    }
256    retval = 1;
257    fclose(fp);
258  }
259  reserve_fd();
260  return retval;
261}
262
263/** Write the access.cnf file.
264 * Writes out the access.cnf file from the linked list
265 */
266void
267write_access_file(void)
268{
269  FILE *fp;
270  char tmpf[BUFFER_LEN];
271  struct access *ap;
272  acsflag *c;
273
274  snprintf(tmpf, BUFFER_LEN, "%s.tmp", ACCESS_FILE);
275  /* Be sure we have a file descriptor */
276  release_fd();
277  fp = fopen(tmpf, FOPEN_WRITE);
278  if (!fp) {
279    do_log(LT_ERR, GOD, GOD, T("Unable to open %s."), tmpf);
280  } else {
281    for (ap = access_top; ap; ap = ap->next) {
282      if (strcmp(ap->host, "@sitelock") == 0) {
283        fprintf(fp, "@sitelock\n");
284        continue;
285      }
286      fprintf(fp, "%s %d ", ap->host, ap->who);
287      switch (ap->can) {
288      case ACS_SITELOCK:
289        break;
290      case ACS_DEFAULT:
291        fprintf(fp, "DEFAULT ");
292        break;
293      default:
294        for (c = acslist; c->name; c++)
295          if (ap->can & c->flag)
296            fprintf(fp, "%s ", c->name);
297        break;
298      }
299      switch (ap->cant) {
300      case ACS_DEFAULT:
301        fprintf(fp, "NONE ");
302        break;
303      default:
304        for (c = acslist; c->name; c++)
305          if (c->toggle && (ap->cant & c->flag))
306            fprintf(fp, "!%s ", c->name);
307        break;
308      }
309      if (ap->comment && *ap->comment)
310        fprintf(fp, "# %s\n", ap->comment);
311      else
312        fprintf(fp, "\n");
313    }
314    fclose(fp);
315    rename_file(tmpf, ACCESS_FILE);
316  }
317  reserve_fd();
318  return;
319}
320
321#ifdef FORCE_IPV4
322static char *
323ip4_to_ip6(const char *addr)
324{
325  static char tbuf1[BUFFER_LEN];
326  char *bp;
327  bp = tbuf1;
328  safe_format(tbuf1, &bp, "::ffff:%s", addr);
329  *bp = '\0';
330  return tbuf1;
331}
332#endif
333
334
335/** Decide if a host can access someway.
336 * \param hname a host or user+host pattern.
337 * \param flag the access type we're testing.
338 * \param who the player attempting access.
339 * \retval 1 access permitted.
340 * \retval 0 access denied.
341 * \verbatim
342 * Given a hostname and a flag decide if the host can do it.
343 * Here's how it works:
344 * We run the linked list and take the first match.
345 *  (If the hostname is user@host, we try to match both user@host
346 *   and just host to each line in the file.)
347 * If we make a match, and the line tells us whether the site can/can't
348 *   do the action, we're done.
349 * Otherwise, we assume that the host can do any toggleable option
350 *   (can create, connect, guest), and don't have any special
351 *   flags (can't register, isn't suspect)
352 * \endverbatim
353 */
354bool
355site_can_access(const char *hname, uint32_t flag, dbref who)
356{
357  struct access *ap;
358  acsflag *c;
359  const char *p;
360
361  if (!hname || !*hname)
362    return 0;
363
364  if ((p = strchr(hname, '@')))
365    p++;
366
367  for (ap = access_top; ap; ap = ap->next) {
368    if (!(ap->can & ACS_SITELOCK)
369        && ((ap->can & ACS_REGEXP)
370            ? (qcomp_regexp_match(ap->re, hname)
371               || (p && qcomp_regexp_match(ap->re, p))
372#ifdef FORCE_IPV4
373               || qcomp_regexp_match(ip4_to_ip6(ap->re), hname)
374               || (p && qcomp_regexp_match(ip4_to_ip6(ap->re), p))
375#endif
376            )
377            : (quick_wild(ap->host, hname)
378               || (p && quick_wild(ap->host, p))
379#ifdef FORCE_IPV4
380               || quick_wild(ip4_to_ip6(ap->host), hname)
381               || (p && quick_wild(ip4_to_ip6(ap->host), p))
382#endif
383            ))
384        && (ap->who == AMBIGUOUS || ap->who == who)) {
385      /* Got one */
386      if (flag & ACS_CONNECT) {
387        if ((ap->cant & ACS_GOD) && God(who))   /* God can't connect from here */
388          return 0;
389        else if ((ap->cant & ACS_WIZARD) && Wizard(who))
390          /* Wiz can't connect from here */
391          return 0;
392        else if ((ap->cant & ACS_ADMIN) && Hasprivs(who))
393          /* Wiz and roy can't connect from here */
394          return 0;
395      }
396      if (ap->cant && ((ap->cant & flag) == flag))
397        return 0;
398      if (ap->can && (ap->can & flag))
399        return 1;
400
401      /* Hmm. We don't know if we can or not, so continue */
402      break;
403    }
404  }
405
406  /* Flag was neither set nor unset. If the flag was a toggle,
407   * then the host can do it. If not, the host can't */
408  for (c = acslist; c->name; c++) {
409    if (flag & c->flag)
410      return c->toggle ? 1 : 0;
411  }
412  /* Should never reach here, but just in case */
413  return 1;
414}
415
416
417/** Return the first access rule that matches a host.
418 * \param hname a host or user+host pattern.
419 * \param who the player attempting access.
420 * \param rulenum pointer to rule position.
421 * \return pointer to first matching access rule or NULL.
422 */
423struct access *
424site_check_access(const char *hname, dbref who, int *rulenum)
425{
426  struct access *ap;
427  const char *p;
428
429  *rulenum = 0;
430  if (!hname || !*hname)
431    return 0;
432
433  if ((p = strchr(hname, '@')))
434    p++;
435
436  for (ap = access_top; ap; ap = ap->next) {
437    (*rulenum)++;
438    if (!(ap->can & ACS_SITELOCK)
439        && ((ap->can & ACS_REGEXP)
440            ? (qcomp_regexp_match(ap->re, hname)
441               || (p && qcomp_regexp_match(ap->re, p))
442#ifdef FORCE_IPV4
443               || qcomp_regexp_match(ip4_to_ip6(ap->host), hname)
444               || (p && qcomp_regexp_match(ip4_to_ip6(ap->host), p))
445#endif
446            )
447            : (quick_wild(ap->host, hname)
448               || (p && quick_wild(ap->host, p))
449#ifdef FORCE_IPV4
450               || quick_wild(ip4_to_ip6(ap->host), hname)
451               || (p && quick_wild(ip4_to_ip6(ap->host), p))
452#endif
453            ))
454        && (ap->who == AMBIGUOUS || ap->who == who)) {
455      /* Got one */
456      return ap;
457    }
458  }
459  return NULL;
460}
461
462/** Display an access rule.
463 * \param ap pointer to access rule.
464 * \param rulenum access rule's number in the list.
465 * \param who unused.
466 * \param buff buffer to store output.
467 * \param bp pointer into buff.
468 * This function provides an appealing display of an access rule
469 * in the list.
470 */
471void
472format_access(struct access *ap, int rulenum,
473              dbref who __attribute__ ((__unused__)), char *buff, char **bp)
474{
475  if (ap) {
476    safe_format(buff, bp, T("Matched line %d: %s %s"), rulenum, ap->host,
477                (ap->can & ACS_REGEXP) ? "(regexp)" : "");
478    safe_chr('\n', buff, bp);
479    safe_format(buff, bp, T("Comment: %s"), ap->comment);
480    safe_chr('\n', buff, bp);
481    safe_str(T("Connections allowed by: "), buff, bp);
482    if (ap->cant & ACS_CONNECT)
483      safe_str(T("No one"), buff, bp);
484    else if (ap->cant & ACS_ADMIN)
485      safe_str(T("All but admin"), buff, bp);
486    else if (ap->cant & ACS_WIZARD)
487      safe_str(T("All but wizards"), buff, bp);
488    else if (ap->cant & ACS_GOD)
489      safe_str(T("All but God"), buff, bp);
490    else
491      safe_str(T("All"), buff, bp);
492    safe_chr('\n', buff, bp);
493    if (ap->cant & ACS_GUEST)
494      safe_str(T("Guest connections are NOT allowed"), buff, bp);
495    else
496      safe_str(T("Guest connections are allowed"), buff, bp);
497    safe_chr('\n', buff, bp);
498    if (ap->cant & ACS_CREATE)
499      safe_str(T("Creation is NOT allowed"), buff, bp);
500    else
501      safe_str(T("Creation is allowed"), buff, bp);
502    safe_chr('\n', buff, bp);
503    if (ap->can & ACS_REGISTER)
504      safe_str(T("Email registration is allowed"), buff, bp);
505    if (ap->can & ACS_SUSPECT)
506      safe_str(T("Players connecting are set SUSPECT"), buff, bp);
507    if (ap->can & ACS_DENY_SILENT)
508      safe_str(T("Denied connections are not logged"), buff, bp);
509  } else {
510    safe_str(T("No matching access rule"), buff, bp);
511  }
512}
513
514
515/** Add an access rule to the linked list.
516 * \param player enactor.
517 * \param host host pattern to add.
518 * \param who player to which rule applies, or AMBIGUOUS.
519 * \param can flags of allowed actions.
520 * \param cant flags of disallowed actions.
521 * \retval 1 success.
522 * \retval 0 failure.
523 * \verbatim
524 * This function adds an access rule after the @sitelock entry.
525 * If there is no @sitelock entry, add one to the end of the list
526 * and then add the entry.
527 * Build an appropriate comment based on the player and date
528 * \endverbatim
529 */
530bool
531add_access_sitelock(dbref player, const char *host, dbref who, uint32_t can,
532                    uint32_t cant)
533{
534  struct access *end;
535  struct access *tmp;
536  const char *errptr = NULL;
537
538
539  tmp = sitelock_alloc(host, who, can, cant, "", &errptr);
540
541  if (!tmp) {
542    notify_format(player, T("Unable to add sitelock entry: %s"), errptr);
543    return false;
544  }
545
546  if (!access_top) {
547    /* Add to the beginning, but first add a sitelock marker */
548    if (!add_access_node("@sitelock", AMBIGUOUS, ACS_SITELOCK, 0, "", &errptr)) {
549      notify_format(player, T("Unable to add @sitelock separator: %s"), errptr);
550      return 0;
551    }
552    access_top->next = tmp;
553  } else {
554    end = access_top;
555    while (end->next && end->can != ACS_SITELOCK)
556      end = end->next;
557    /* Now, either we're at the sitelock or the end */
558    if (end->can != ACS_SITELOCK) {
559      /* We're at the end and there's no sitelock marker. Add one */
560      if (!add_access_node("@sitelock", AMBIGUOUS, ACS_SITELOCK, 0, "",
561                           &errptr)) {
562        notify_format(player, T("Unable to add @sitelock separator: %s"),
563                      errptr);
564        return 0;
565      }
566      end = end->next;
567    } else {
568      /* We're in the middle, so be sure we keep the list linked */
569      tmp->next = end->next;
570    }
571    end->next = tmp;
572  }
573  return 1;
574}
575
576/** Remove an access rule from the linked list.
577 * \param pattern access rule host pattern to match.
578 * \return number of rule removed.
579 * This function removes an access rule from the list.
580 * Only rules that appear after the "@sitelock" rule can be
581 * removed with this function.
582 */
583int
584remove_access_sitelock(const char *pattern)
585{
586  struct access *ap, *next, *prev = NULL;
587  int n = 0;
588
589  /* We only want to be able to delete entries added with @sitelock */
590  for (ap = access_top; ap; ap = ap->next)
591    if (strcmp(ap->host, "@sitelock") == 0) {
592      prev = ap;
593      ap = ap->next;
594      break;
595    }
596
597  while (ap) {
598    next = ap->next;
599    if (strcasecmp(pattern, ap->host) == 0) {
600      n++;
601      if (ap->re)
602        free(ap->re);
603      mush_free(ap, "sitelock.rule");
604      if (prev)
605        prev->next = next;
606      else
607        access_top = next;
608    } else {
609      prev = ap;
610    }
611    ap = next;
612  }
613
614  return n;
615}
616
617/* Free the entire access list */
618static void
619free_access_list(void)
620{
621  struct access *ap, *next;
622  ap = access_top;
623  while (ap) {
624    next = ap->next;
625    if (ap->re)
626      free(ap->re);
627    mush_free(ap, "sitelock.rule");
628    ap = next;
629  }
630  access_top = NULL;
631}
632
633
634/** Display the access list.
635 * \param player enactor.
636 * Sends the complete access list to the player.
637 */
638void
639do_list_access(dbref player)
640{
641  struct access *ap;
642  acsflag *c;
643  char flaglist[BUFFER_LEN];
644  int rulenum = 0;
645  char *bp;
646
647  for (ap = access_top; ap; ap = ap->next) {
648    rulenum++;
649    if (ap->can != ACS_SITELOCK) {
650      bp = flaglist;
651      for (c = acslist; c->name; c++) {
652        if (c->flag == ACS_DEFAULT)
653          continue;
654        if (ap->can & c->flag) {
655          safe_chr(' ', flaglist, &bp);
656          safe_str(c->name, flaglist, &bp);
657        }
658        if (c->toggle && (ap->cant & c->flag)) {
659          safe_chr(' ', flaglist, &bp);
660          safe_chr('!', flaglist, &bp);
661          safe_str(c->name, flaglist, &bp);
662        }
663      }
664      *bp = '\0';
665      notify_format(player,
666                    "%3d SITE: %-20s  DBREF: %-6s FLAGS:%s", rulenum,
667                    ap->host, unparse_dbref(ap->who), flaglist);
668      notify_format(player, " COMMENT: %s", ap->comment);
669    } else {
670      notify(player,
671             T
672             ("---- @sitelock will add sites immediately below this line ----"));
673    }
674
675  }
676  if (rulenum == 0) {
677    notify(player, T("There are no access rules."));
678  }
679}
680
681/** Parse access options into fields.
682 * \param opts access options to read from.
683 * \param who pointer to player to whom rule applies, or AMBIGUOUS.
684 * \param can pointer to flags of allowed actions.
685 * \param cant pointer to flags of disallowed actions.
686 * \param player enactor.
687 * \return number of options successfully parsed.
688 * Parse options and return the appropriate can and cant bits.
689 * Return the number of options successfully parsed.
690 * This makes a copy of the options string, so it's not modified.
691 */
692int
693parse_access_options(const char *opts, dbref *who, uint32_t * can,
694                     uint32_t * cant, dbref player)
695{
696  char myopts[BUFFER_LEN];
697  char *p;
698  char *w;
699  acsflag *c;
700  int found, totalfound, first;
701
702  if (!opts || !*opts)
703    return 0;
704  strcpy(myopts, opts);
705  totalfound = 0;
706  first = 1;
707  if (who)
708    *who = AMBIGUOUS;
709  p = trim_space_sep(myopts, ' ');
710  while ((w = split_token(&p, ' '))) {
711    found = 0;
712
713    if (first && who) {         /* Check for a character */
714      first = 0;
715      if (is_integer(w)) {      /* We have a dbref */
716        *who = parse_integer(w);
717        if (*who != AMBIGUOUS && !GoodObject(*who))
718          *who = AMBIGUOUS;
719        continue;
720      }
721    }
722
723    if (*w == '!') {
724      /* Found a negated warning */
725      w++;
726      for (c = acslist; c->name; c++) {
727        if (c->toggle && !strncasecmp(w, c->name, strlen(c->name))) {
728          *cant |= c->flag;
729          found++;
730        }
731      }
732    } else {
733      /* None is special */
734      if (!strncasecmp(w, "NONE", 4)) {
735        *cant = ACS_DEFAULT;
736        found++;
737      } else {
738        for (c = acslist; c->name; c++) {
739          if (!strncasecmp(w, c->name, strlen(c->name))) {
740            *can |= c->flag;
741            found++;
742          }
743        }
744      }
745    }
746    /* At this point, we haven't matched any warnings. */
747    if (!found) {
748      if (GoodObject(player))
749        notify_format(player, T("Unknown access option: %s"), w);
750      else
751        do_log(LT_ERR, GOD, GOD, T("Unknown access flag: %s"), w);
752    } else {
753      totalfound += found;
754    }
755  }
756  return totalfound;
757}
Note: See TracBrowser for help on using the browser.