/* #ident "@(#)smail/src:RELEASE-3_2_0_121:match.c,v 1.40 2005/07/13 17:15:04 woods Exp" */ /* * Copyright (C) 2004 Greg A. Woods * * See the file COPYING, distributed with smail, for restriction * and warranty information. * * Some or all of this code may also be released under a separate copyright * license. Contact the author for details. */ /* * match.c: * routines for matching values in various ways. * * external functions: match_re, match_ip, match_ip_net, flip_inet_addr, * match_re_list, compile_pcre_list, match_pcre_list, * free_pcre_list, format_pcre_list, trusted_invoker */ #include "defs.h" #ifdef STANDALONE # undef HAVE_LIBWHOSON #endif #include #include #include #include #include #include #include #include #include #ifdef STDC_HEADERS # include # include #else # ifdef HAVE_STDLIB_H # include # endif #endif #ifdef __STDC__ # include #else # include #endif #ifdef HAVE_STRING_H # if !defined(STDC_HEADERS) && defined(HAVE_MEMORY_H) # include # endif # include #endif #ifdef HAVE_STRINGS_H # include #endif #if defined(HAVE_UNISTD_H) # include #endif #ifdef HAVE_LIBWHOSON # include #endif #include #include /* PCRE's POSIX-like API */ #include "smail.h" #include "main.h" #include "parse.h" #include "addr.h" #include "route.h" #include "alloc.h" #include "list.h" #include "smailsock.h" #include "smailstring.h" #include "dys.h" #include "log.h" #include "main.h" #include "match.h" #ifdef HAVE_BIND # include "bindsmtpth.h" # include "bindlib.h" #endif #include "exitcodes.h" #include "smailconf.h" #include "extern.h" #include "debug.h" #include "smailport.h" /* functions local to this file */ static pcre_info_t *compile_pcre_expr __P((char *, int, char *, char *, int, char **)); static int match_pcre_expr __P((char *, size_t, pcre_info_t *, int, char **)); static char *pcre_expand_msg __P((pcre_info_t *, char *, int *, int)); /* * match_re - match a string against a regular expression using the POSIX API * * NOTE: If icase is non-zero then the goal is to do case-insensitive matching * by default. * * RETURNS: * * MATCH_MATCHED - the string matched the RE * MATCH_NOMATCH - no match. * MATCH_FAIL - an error occured, error message written to paniclog */ int match_re(key, exp, icase, start, beg, end, last) char *key; /* string to match */ char *exp; /* RE to match against */ int icase; /* ignore case? */ char **start; /* optional ptr to beg of match */ char **beg; /* optional ptr to beg of sub-expression */ char **end; /* optional ptr to after sub-expression */ char **last; /* optional ptr to after match */ { regex_t preg; char re_errmsg[BUFSIZ]; regmatch_t pmatch[2]; int rc; DEBUG3(DBG_ADDR_HI, "match_re: matching '%v' against expression '%v'%s\n", key, exp, icase ? ", ignoring case" : ""); /* XXX should consider converting to using the native PCRE API, perhaps * compile_pcre_expr() and match_pcre_expr()... */ if ((rc = regcomp(&preg, exp, (REG_EXTENDED | (icase ? REG_ICASE : 0) | ((beg || end) ? 0 : REG_NOSUB))))) { (void) regerror(rc, &preg, re_errmsg, sizeof(re_errmsg)); write_log(WRITE_LOG_PANIC, "match_re: bad regular expression '%v': %s", exp, re_errmsg); regfree(&preg); return MATCH_FAIL; } /* zero indicates match */ rc = regexec(&preg, key, (size_t) 2, pmatch, 0); switch (rc) { case 0: rc = MATCH_MATCHED; if (start) { *start = key + pmatch[0].rm_so; } /* if (pmatch[1].rm_so == -1) assert(pmatch[1].rm_eo == -1); */ if (beg) { *beg = key + ((pmatch[1].rm_so == -1) ? pmatch[0].rm_so : pmatch[1].rm_so); } if (end) { *end = key + ((pmatch[1].rm_eo == -1) ? pmatch[0].rm_eo : pmatch[1].rm_eo); } if (last) { *last = key + pmatch[0].rm_eo; } break; case REG_NOMATCH: rc = MATCH_NOMATCH; break; default: (void) regerror(rc, &preg, re_errmsg, sizeof(re_errmsg)); write_log(WRITE_LOG_PANIC, "match_re: error (%d) matching '%v' with expression '%v': %s", rc, REG_NOMATCH, key, exp, re_errmsg); rc = MATCH_FAIL; /* error during matching */ } regfree(&preg); DEBUG3(DBG_ADDR_HI, "match_re: '%v' %s expression '%v'\n", key, (rc == MATCH_MATCHED) ? "matched" : ((rc == MATCH_NOMATCH) ? "did not match" : "triggered REGEX error"), exp); return rc; } /* * match_re_list - match a string against a list of REs * * Takes a list of RE:RE:RE and compares each against the string until a match * is found or the list is exhausted. * * RE is a Regular Expression. * * RE may optionally be preceded by "![ \t]*" which will reverse the return * value (i.e. immediately return 0 instead of 1) if the RE matches. * * RE may optionally be followed by ';message text', which of course must not * contain a ':' * * WARNING: cannot diddle with "patterns" storage -- may be read-only string. * * RETURNS: MATCH_MATCHED if matched, else MATCH_NOMATCH (unless the matching * RE was prefixed by '!', in which case it returns the opposite), or * MATCH_FAIL if anything goes wrong. * * if a match is found and if a sub-field accompanies the matching item then * the *reasonp will be set to point to a copy of its value too. * */ int match_re_list(str, patterns, icase, reasonp) char *str; /* string we are matching */ char *patterns; /* list of patterns we are using to match it */ int icase; /* ignore case? */ char **reasonp; /* optional pointer for returned message string */ { size_t str_len = strlen(str); char *ent_p; char *end_ent; char *no_reason; if (!patterns) { /* XXX too bad we don't know the name of the patterns parameter here... */ DEBUG1(DBG_ADDR_HI, "match_re_list(): null patterns pointer, nothing to do for %v...\n", str); return MATCH_NOMATCH; } /* XXX should the second parameter be limited to, say, 40 chars? */ DEBUG3(DBG_ADDR_HI, "match_re_list(%v, %v, %s): called...\n", str, patterns, icase ? "icase" : "0"); if (!reasonp) { reasonp = &no_reason; } *reasonp = NULL; /* run through the pattern list */ for (ent_p = patterns; /* NOTEST */ ; ent_p = end_ent + 1) { int matchresult = MATCH_MATCHED; char prev_c; /* skip any spaces at the beginning */ while (*ent_p && isspace((int) *ent_p)) { ent_p++; } if (*ent_p == '\0') { DEBUG(DBG_MAIN_HI, "match_re_list(): skipping empty final entry.\n"); break; } /* * find the end of this entry, i.e. the next unescaped ':' * * Note there are no other internal quoting mechanisms (as there are in * the full PCRE lists) except the backslash so we don't have to worry * about any other quoting delimiter. */ end_ent = ent_p; prev_c = '\0'; while (*end_ent && (*end_ent != ':' || prev_c == '\\')) { prev_c = *end_ent; end_ent++; } /* only dig deeper into non-empty entries */ if (end_ent > ent_p) { char *start_re = ent_p; char *end_re = NULL; static struct str exp; static int exp_inited = FALSE; static struct str msg; static int msg_inited = FALSE; pcre_info_t *re_info; int result; /* check for a negation sign */ if (*start_re == '!') { start_re++; matchresult = MATCH_NOMATCH; /* a match of a negated item... */ /* skip any spaces after the '!' */ while (*start_re && start_re <= end_ent && isspace((int) *start_re)) { start_re++; } } /* * search for the end of the RE, i.e. the next un-escaped ';', or * the end of the entry, or the end of the string, whichever comes * first */ end_re = start_re; prev_c = '\0'; while (*end_re && (*end_re != ';' || prev_c == '\\') && end_re < end_ent) { prev_c = *end_re; end_re++; } if (start_re == end_re) { /* skip empty REs */ DEBUG(DBG_MAIN_HI, "match_re_list(): skipping empty RE\n"); continue; } if (!exp_inited) { STR_INIT(&exp); exp_inited = TRUE; } else { STR_CHECK(&exp); STR_CLEAR(&exp); } STR_NEXT(&exp, '^'); /* create a fully anchored RE pattern */ STR_NCAT(&exp, start_re, (size_t) (end_re - start_re)); STR_CAT(&exp, "$"); /* NUL-terminates the string too */ if (!msg_inited) { STR_INIT(&msg); msg_inited = TRUE; } else { STR_CHECK(&msg); STR_CLEAR(&msg); } if (*end_re == ';') { /* note we're skipping the ';' itself */ STR_NCAT(&msg, end_re + 1, (size_t) (end_ent - end_re - 1)); STR_NEXT(&msg, '\0'); } /* RE lists can be dynamic so we cannot cache this... */ if (!(re_info = compile_pcre_expr(STR(&exp), '/', icase ? "i" : "", STR(&msg), matchresult, reasonp))) { /* exp and msg are reused... */ return MATCH_FAIL; } result = match_pcre_expr(str, str_len, re_info, TRUE, reasonp); /* * note that since we never pre-compile these patterns we could * just use matchresult without resetting it from re_info, but * doing it this way preserves the compile_pcre_expr() API. */ matchresult = re_info->matchresult; /* note re_info->{re,msg} are reused and re_info->opts is static */ xfree((char *) re_info->code); xfree((char *) re_info); re_info = NULL; switch (result) { case MATCH_NOMATCH: /* just continue after the matching so the end-of-list test works! */ break; case MATCH_MATCHED: DEBUG4(DBG_MAIN_MID, "match_re_list(): '%s%v' %s: returning %s\n", matchresult == MATCH_NOMATCH ? "! " : "", str, result == MATCH_MATCHED ? "matched" : "notmatched", matchresult == MATCH_MATCHED ? "matched" : "NOT-matched"); return matchresult; case MATCH_FAIL: DEBUG1(DBG_MAIN_MID, "match_re_list(): error: %s\n", *reasonp); return MATCH_FAIL; #if 0 /* #ifndef NDEBUG */ default: assert(FALSE); #endif } /* continue with next pattern */ } else { DEBUG(DBG_MAIN_HI, "match_re_list(): skipping empty list entry\n"); } if (*end_ent == '\0') { break; /* if there is one */ } } DEBUG2(DBG_ADDR_HI, "string '%v' does not match in '%v'\n", str, patterns); return MATCH_NOMATCH; } #ifdef HAVE_BSD_NETWORKING /* * match_ip - match an ip against a list of host/net numbers * * Takes a list of IP:IP:IP and compares against an IP given in ASCII. * * 'IP' is an IP number or IP/bits CIDR network spec. * * IP may optionally be preceded by "![ \t]*" which will reverse the return * value (i.e. immediately return 0 instead of 1) if the IP matches. * * IP may optionally be followed by ';message text', which of course must not * contain a ':' * * Note that the field separator shown above as ':' is specified by the caller, * as is message sub-field separator show above as ';'. Normally the field * separator will be either ':' or ',' and the sub-field separator will be ';' * or '\0' (i.e. none). * * WARNING: cannot diddle with pattern -- may be read-only string. * * RETURNS: TRUE if matched, else FALSE (unless the IP was prefixed by '!', in * which case it returns the opposite). * * WARNING: does not use the MATCH_* returns -- callers only expect boolean. * * If a match is found and if a sub-field accompanies the matching item then * the *reasonp will be set to point to a copy of its value too. * * WARNING: There is no error reporting capability in this function's API. If * the pattern is mal-formed then in theory it can never be matched and in * theory if a mal-formed pattern is negated with a '!' then 1 should be * returned immediately. However this is not currently the case since given * the way this function is used such behaviour would most likely cause an * uninentional wildcard match. * * XXX allow_wildcard isn't actually used and should be zapped. */ unsigned int match_ip(ip, patterns, fldsep, msgsep, allow_wildcard, reasonp) /*const*/ char *ip; /* ip address we are matching */ /*const*/ char *patterns; /* list of patterns we are using to match it */ int fldsep; /* field separator character */ int msgsep; /* optional message string separator */ int allow_wildcard; /* allow 0/0? */ char **reasonp; /* optional pointer for returned message string */ { /*const*/ char *pat_p; /*const*/ char *end_p; size_t len; if (!patterns) { DEBUG1(DBG_ADDR_HI, "match_ip(): null patterns pointer, nothing to do for %v...\n", ip); return FALSE; } DEBUG4(DBG_ADDR_HI, "match_ip(%v, %v, %c, %c): called...\n", ip, patterns, fldsep, msgsep); if (reasonp) { *reasonp = NULL; } /* run through the pattern list */ for (pat_p = patterns; /* NOTEST */ ; pat_p = end_p + 1) { int matchresult = TRUE; char prev_c; /* skip any spaces at front */ while (*pat_p && isspace((int) *pat_p)) { pat_p++; } if (*pat_p == '\0') { DEBUG(DBG_MAIN_HI, "match_ip(): skipping empty final entry.\n"); break; } /* find the end of this entry */ end_p = pat_p; prev_c = '\0'; while (*end_p && (*end_p != fldsep || prev_c == '\\')) { prev_c = *end_p; end_p++; } if (end_p > pat_p && *pat_p == '!') { pat_p++; matchresult = FALSE; /* a match of a negated item... */ /* skip any spaces after the '!' */ while (*pat_p && pat_p <= end_p && isspace((int) *pat_p)) { pat_p++; } } /* skip empty patterns */ /* XXX maybe we could allow empty patterns with "; text" too */ len = end_p - pat_p; if (len > 0) { char *p; static struct str exp; static int inited = FALSE; if (!inited) { STR_INIT(&exp); inited = TRUE; } else { STR_CHECK(&exp); STR_CLEAR(&exp); } STR_NCAT(&exp, pat_p, len); STR_NEXT(&exp, '\0'); p = NULL; if (msgsep && (p = strchr(STR(&exp), msgsep))) { *p = '\0'; p = chop(p + 1); } chop(STR(&exp)); /* warn if we have a deprecated wild-card at end of the current pattern */ if (*(end_p - 1) == '*') { /* * this could get rather noisy for the first time around, but * keeping track of which ones we've warned about, even per * process, is damn near impossible to do safely. */ write_log(WRITE_LOG_PANIC, "match_ip(): an unsupported wildcard pattern [%v] was found in '%v'!", STR(&exp), patterns); } /* XXX localnet should be deprecated */ if (strcmpic("localnet", STR(&exp)) == 0) { DEBUG2(DBG_ADDR_HI, "match_ip(): found 'localnet' in list...\n", ip, smtp_local_net); /* our pattern will be the string in smtp_local_net */ if (!smtp_local_net) { DEBUG1(DBG_ADDR_HI, "match_ip(): nil smtp_local_net cannot match %v...\n", ip); /* continue with next pattern */ } else { switch (match_ip_net(ip, smtp_local_net, allow_wildcard)) { case MATCH_NOMATCH: break; /* not this one! */ case MATCH_FAIL: /* this should be almost impossible for smtp_local_net */ write_log(WRITE_LOG_PANIC, "match_ip(): invalid smtp_local_net [%v] was found for localnet in '%v'!", smtp_local_net, patterns); break; default: if (p && reasonp) { *reasonp = COPY_STRING(p); } DEBUG5(DBG_ADDR_MID, "ip [%v] matches %slocalnet [%v]%s%v\n", ip, matchresult ? "" : "!", smtp_local_net, (reasonp && *reasonp) ? " with comment: " : "", (reasonp && *reasonp) ? *reasonp : ""); return matchresult; } } /* continue with next pattern */ #ifdef HAVE_LIBWHOSON } else if (strcmpic("whoson", STR(&exp)) == 0) { int wso; char retbuf[BUFSIZ]; DEBUG1(DBG_ADDR_HI, "(checking if [%v] is listed in the 'whoson' database)\n", ip); *retbuf = '\0'; wso = wso_query(ip, retbuf, (int) sizeof(retbuf)); retbuf[sizeof(retbuf) - 1] = '\0'; /* just in case... */ if (wso < 0 && *retbuf) { /* XXX hope this doesn't flood the log! */ write_log(WRITE_LOG_PANIC, "wso_query(%v): whoson query failed: %v", ip, retbuf); /* continue with next pattern */ } else if (wso == 0) { DEBUG3(DBG_ADDR_MID, "ip [%v] found by whoson: %s%v.\n", ip, matchresult ? "" : "!", retbuf); return matchresult; } else { DEBUG1(DBG_ADDR_MID, "ip [%v] NOT found by whoson\n", ip); /* continue with next pattern */ } #endif } else { switch (match_ip_net(ip, STR(&exp), allow_wildcard)) { case MATCH_NOMATCH: break; /* not this one! */ case MATCH_FAIL: write_log(WRITE_LOG_PANIC, "match_ip(): invalid netspec [%v] was found in '%v'!", STR(&exp), patterns); break; default: if (p && reasonp) { *reasonp = COPY_STRING(p); } DEBUG5(DBG_ADDR_MID, "ip [%v] matches IP/network of %s[%v]%s%v\n", ip, matchresult ? "" : "!", STR(&exp), (reasonp && *reasonp) ? " with comment: " : "", (reasonp && *reasonp) ? *reasonp : ""); return matchresult; } /* continue with next pattern */ } } if (*end_p == '\0') { break; } } DEBUG2(DBG_ADDR_HI, "match_ip(): [%v] does not match any IP/network in '%v'\n", ip, patterns); return FALSE; } /* * RETURNS: MATCH_MATCHED, MATCH_NOMATCH, or MATCH_FAIL on error. * * XXX allow_wildcard isn't actually used and should be zapped. */ int match_ip_net(ipaddr, netspec, allow_wildcard) char *ipaddr; char *netspec; int allow_wildcard; { in_addr_t dest; in_addr_t net; in_addr_t mask; int bits = 0; DEBUG2(DBG_ADDR_HI, "match_ip_net([%v], [%v]) called...\n", ipaddr, netspec); net = 0; /* net must be pre-initialized */ if ((bits = inet_net_pton(AF_INET, netspec, (void *) &net, sizeof(net))) == -1) { DEBUG2(DBG_ADDR_LO, "match_ip_net(): [%v] is invalid: %s\n", netspec, strerror(errno)); return MATCH_FAIL; } if (bits > 32) { /* note this is normally impossible with a correct inet_net_pton() impl. */ DEBUG2(DBG_ADDR_LO, "match_ip_net(): [%v] has invalid number of bits: %d\n", netspec, bits); return MATCH_FAIL; } if (!bits && net) { /* * note some inet_net_pton() implementations still use inet_addr() * classful parsing syntax internally and as a result "0/0", for * example, may not work and "0.0.0.0/0" may be needed instead. */ DEBUG1(DBG_ADDR_LO, "match_ip_net(): wildcard [%v] has non-zero bits in network part\n", netspec); return MATCH_FAIL; } if (!bits && !net && !allow_wildcard) { /* all zero bits is a proper wildcard */ DEBUG1(DBG_ADDR_LO, "match_ip_net(): [%s] is an all zeros wildcard.\n", netspec); return MATCH_FAIL; } /* * Note: ((u_int32_t) << 32) is undefined according to the ANSI C spec. * Hence the extra test for bits == 0. */ mask = (bits == 0) ? 0 : (0xffffffffL << (32 - bits)); net = ntohl(net); if (mask && !(net & mask)) { /* should never match anything valid */ DEBUG2(DBG_ADDR_MID, "match_ip_net(): warning: [%v] has all zero bits in net part (0x%x).\n", netspec, net & mask); } if (mask && (net & ~mask)) { /* was giving a host address for the netspec intentional? */ DEBUG2(DBG_ADDR_MID, "match_ip_net(): warning: [%v] has non-zero bits in host part (0x%x).\n", netspec, net & ~mask); } dest = 0; /* dest probably should be pre-initialized */ switch (inet_pton(AF_INET, ipaddr, &dest)) { case 0: DEBUG1(DBG_ADDR_LO, "match_ip_net(): [%s] is not parsable.\n", ipaddr); return MATCH_FAIL; case -1: DEBUG2(DBG_ADDR_LO, "match_ip_net(): [%s] is invalid: %s\n", ipaddr, strerror(errno)); return MATCH_FAIL; } dest = ntohl(dest); #if 0 /* noisy! for really heavy duty debugging only! */ DEBUG4(DBG_ADDR_HI, "match_ip_net(): dest=0x%x, net=0x%x, bits=%d, mask=0x%x\n", dest, net, bits, mask); #endif return ((dest & mask) == (net & mask)); } /* * match_dnsbl - look up subdomain in list of domains given in dnsbl * * - dnsbl is a colon-separated list of domains in which an A RR for * the specified name is looked up. * * - Optionally a comma-separated list of valid A RR values, either as explicit * 4-octet ascii-form host addresses, or in a network/mask form, follows a * semicolon after any domain (given in form suitable for a config file entry): * * :dns.bl.domain; 127.0.0.1, 127.0.0.4 * :another.dns.bl.domain; 127/8 * * Returns 1 if any matched, else 0. * * if a match is found then *matchp will be set to the fully expanded * domain that matched and *addrp will be set to the A RR's value, both as * ASCII strings; and as well if a TXT RR is found at the same domain then * the *msgp will be set to point to its value too. */ int match_dnsbl(target, dnsbl, dnsbl_name, matchp, addrp, msgp) char *target; /* subdomain to match */ char *dnsbl; /* DNS BlackList domain list */ char *dnsbl_name; /* name of the DNSBL */ char **matchp; /* pointer to hold formatted match address */ char **addrp; /* pointer to hold ASCII A RR value address */ char **msgp; /* pointer to hold TXT message pointer */ { static int dns_took_too_long = FALSE; static int rbl_took_too_long = FALSE; char *pat_p; char *end_p; int rc = FALSE; size_t len; time_t rbl_start_time; time_t rbl_end_time; time_t start_getone_time; time_t end_getone_time; #define MAX_RES_TIMEOUT ((RES_TIMEOUT * 4/*retry*/ * MAXNS) - 10) /* XXX is this too long? */ #define MAX_DNSBL_TIMEOUT ((smtp_receive_command_timeout / 2) - 2) if (msgp) { *msgp = NULL; } if (!dnsbl) { DEBUG1(operation_mode == TEST_DNSBL_MODE ? 0 : DBG_ADDR_HI, "match_dnsbl(): DNSBL %s is empty\n", dnsbl_name); return 0; } DEBUG2(DBG_ADDR_HI, "match_dnsbl(): beginning search for %s DNSBL %s\n", target, dnsbl_name); /* run through the pattern list */ time(&rbl_start_time); for (pat_p = dnsbl; /* NOTEST */ ; pat_p = end_p + 1) { char *a_rr_pats; /* * this matches any de facto standard RBL value. Don't use 0/0 here! * Remember the fiasco with relayips.shub-inter.net! */ a_rr_pats = "127/8"; /* skip any spaces at front */ while (*pat_p && isspace((int) *pat_p)) { pat_p++; } /* find the end of the next pattern */ end_p = pat_p; while (*end_p && *end_p != ':' && !isspace((int) *pat_p)) { end_p++; } /* XXX chop spaces off the end too? */ /* skip empty patterns */ len = end_p - pat_p; if (len > 0) { char *bl_hname; char *txtaddr; size_t bl_hname_len = strlen(target) + len + 1; /* one more for the '.' */ char *p; struct hostent *hp; /* WARNING: we can't diddle with dnsbl -- it may be read-only string. */ bl_hname = xmalloc(bl_hname_len + 1); strcpy(bl_hname, target); strcat(bl_hname, "."); strncat(bl_hname, pat_p, len); bl_hname[bl_hname_len] = '\0'; DEBUG1(DBG_ADDR_HI, "match_dnsbl(): working on DNSBL spec '%v'\n", bl_hname); if ((p = strchr(bl_hname, ';'))) { a_rr_pats = p; *a_rr_pats = '\0'; a_rr_pats++; DEBUG1(DBG_ADDR_HI, "match_dnsbl(): found explicit IP pattern(s) [%v]\n", a_rr_pats); } DEBUG2(DBG_ADDR_MID, "match_dnsbl(): looking for an A RR matching [%v] at '%v'\n", a_rr_pats, bl_hname); /* * Do we (really?) care if there's a temporary server failure when * trying to look up an DNSBL? If we were very serious about * blocking every possible piece of unwanted e-mail then we would * be, I guess. That would rather complicate things for callers * though because then we'd have to return a three (or four) state * value insead of a binary result, and also any relevant * information about why the query failed. */ time(&start_getone_time); if ((hp = gethostbyname(bl_hname))) { struct in_addr bl_hname_addr; time(&end_getone_time); if (!dns_took_too_long && (end_getone_time - start_getone_time) > MAX_RES_TIMEOUT) { write_log(WRITE_LOG_PANIC, "successful DNS lookup for %s in %s took %ld seconds!", bl_hname, dnsbl_name, (long int) (end_getone_time - start_getone_time)); dns_took_too_long = TRUE; } else { DEBUG2(DBG_ADDR_HI, "match_dnsbl(): successful DNS lookup for %s took %ld seconds\n", bl_hname, (long int) (end_getone_time - start_getone_time)); } memcpy((char *) &bl_hname_addr, hp->h_addr_list[0], sizeof(struct in_addr)); txtaddr = COPY_STRING(inet_ntoa(bl_hname_addr)); if (match_ip(txtaddr, a_rr_pats, ',', '\0', FALSE, (char **) NULL)) { struct error *junkerr; /* NOTE: don't free bl_hname or txtaddr here -- they are returned! */ *matchp = bl_hname; *addrp = txtaddr; if (msgp) { *msgp = bind_lookup_txt_rr(bl_hname, &junkerr); } DEBUG6(DBG_ADDR_LO, "match_dnsbl(): found an A RR matching [%v] at DNSBL %v%s%s%v%s\n", a_rr_pats, bl_hname, !msgp ? " (with no TXT RR lookup)" : "", msgp && *msgp ? "\n\twith associated TXT RR(s)\n\t``" : ": error getting TXT RR: ", msgp && *msgp ? *msgp : (junkerr ? junkerr->message : "NO DATA"), msgp && *msgp ? "''" : ""); /* XXX if debug > 9 then junkerr leaks */ time(&rbl_end_time); if (!rbl_took_too_long && (rbl_end_time - rbl_start_time) > MAX_DNSBL_TIMEOUT) { write_log(WRITE_LOG_PANIC, "partial search through %s DNSBL took %ld seconds!", dnsbl_name, (long int) (end_getone_time - start_getone_time)); rbl_took_too_long = TRUE; } else { DEBUG3(DBG_ADDR_MID, "match_dnsbl(): partial search for target %s in DNSBL %s took %ld secs\n", target, dnsbl_name, (long int) (rbl_end_time - rbl_start_time)); } rc = TRUE; if (debug < 9) { return rc; } else { /* * This stuff may leak (especially if more than one * match is found), but only during debugging. */ *matchp = COPY_STRING(bl_hname); *addrp = COPY_STRING(txtaddr); *msgp = COPY_STRING(*msgp); } } xfree(txtaddr); } else { time(&end_getone_time); if (!dns_took_too_long && (end_getone_time - start_getone_time) > MAX_RES_TIMEOUT) { write_log(WRITE_LOG_PANIC, "empty DNS lookup for %s in %s took %ld seconds!", bl_hname, dnsbl_name, (long int) (end_getone_time - start_getone_time)); dns_took_too_long = TRUE; } else { DEBUG2(DBG_ADDR_HI, "match_dnsbl(): empty DNS lookup for %s took %ld seconds\n", bl_hname, (long int) (end_getone_time - start_getone_time)); } } xfree(bl_hname); } if (*end_p == '\0') { break; } } time(&rbl_end_time); if (!rbl_took_too_long && (rbl_end_time - rbl_start_time) > MAX_DNSBL_TIMEOUT) { write_log(WRITE_LOG_PANIC, "Complete search through %s DNSBL took %ld seconds!", dnsbl_name, (long int) (rbl_end_time - rbl_start_time)); rbl_took_too_long = TRUE; } DEBUG3(DBG_ADDR_LO, "match_dnsbl(): target %s not found in DNSBL %s (after %ld secs)\n", target, dnsbl_name, (long int) (rbl_end_time - rbl_start_time)); return rc; } #define CHOP_B(x) ((int) ((x) & 0xff)) /* * flip the octets of an Internet address, e.g. to create an in-addr.arpa name */ char * flip_inet_addr(inaddr) char *inaddr; { in_addr_t addr; addr = get_inet_addr(inaddr); addr = ntohl(addr); return xprintf("%u.%u.%u.%u", CHOP_B(addr), CHOP_B(addr >> 8), CHOP_B(addr >> 16), CHOP_B(addr >> 24)); } #undef CHOP_B #endif /* HAVE_BSD_NETWORKING */ /* * compile and study the REs in re_list producing a voidplist_t */ voidplist_t * compile_pcre_list(re_list, errorp) char *re_list; char **errorp; { voidplist_t *nl = NULL; voidplist_t *cur; voidplist_t *next; voidplist_t *new; /*const*/ char *ent_p; /*const*/ char *end_ent; *errorp = NULL; if (! re_list) { return NULL; } /* run through the pattern list */ for (ent_p = re_list; /* NOTEST */ ; ent_p = end_ent + 1) { char prev_c; int delim_c; /*const*/ char *start_re; /*const*/ char *end_re = NULL; char *p; struct str exp; struct str opts; struct str msg; size_t msglen; pcre_info_t *re_info; int matchresult; /* skip any spaces at the beginning */ while (*ent_p && isspace((int) *ent_p)) { ent_p++; } if (*ent_p == '\0') { DEBUG(DBG_MAIN_HI, "compile_pcre_list(): skipping empty final entry.\n"); break; } /* * the first thing in an entry is the RE */ start_re = end_ent = ent_p; /* * unless it is the standard '!' negation character, in which case the * RE starts at the next non-space char.... */ if (*start_re == '!') { start_re++; matchresult = MATCH_NOMATCH; /* a match of a negated item... */ /* skip any spaces after the '!' */ while (*start_re && isspace((int) *start_re)) { start_re++; } } else { matchresult = MATCH_MATCHED; } /* * the first non-space char of an RE is the RE delimiter char */ delim_c = *start_re++; if (delim_c == ':') { DEBUG(DBG_MAIN_HI, "compile_pcre_list(): skipping empty entry.\n"); continue; /* XXX is this always safe? */ } if (delim_c == ';') { *errorp = xprintf("';' is not a valid RE delimiter"); return NULL; } /* * walk the entry end pointer up to the end of the RE -- i.e. the next * unescaped delim_c */ end_ent = start_re + 1; if (*end_ent == '\0') { *errorp = xprintf("list ends before last RE's closing '%c' delimiter", delim_c); return NULL; } prev_c = '\0'; while (*end_ent && (*end_ent != delim_c || prev_c == '\\')) { prev_c = *end_ent; end_ent++; } end_re = end_ent; if (*end_re != delim_c) { *errorp = xprintf("RE '%S' has no closing '%c' delimiter", (size_t) (end_ent - ent_p), ent_p, delim_c); return NULL; } /* * walk the the end pointer past any options and message text up to the * beginning of the next list entry, i.e. the next un-escaped ':' */ prev_c = '\0'; while (*end_ent && (*end_ent != ':' || prev_c == '\\')) { prev_c = *end_ent; end_ent++; } /* don't do this check until after end_ent points to any next entry */ if (start_re == end_re) { /* skip empty patterns */ DEBUG(DBG_MAIN_HI, "compile_pcre_list(): skipping empty RE\n"); /* this should be safe */ continue; } STR_INIT(&exp); /* do not re-use */ STR_NCAT(&exp, start_re, (size_t) (end_re - start_re)); STR_NEXT(&exp, '\0'); /* NUL-terminates the string */ /* point past the final delimiter again */ end_re++; /* get (optional) options: "/RE/options" */ for (p = end_re; p <= end_ent && *p && *p != ';' && *p != ':'; p++) { ; /* NO-OP */ } if (p > end_re) { STR_INIT(&opts); /* do not re-use */ STR_NCAT(&opts, end_re, (size_t) (p - end_re)); STR_NEXT(&opts, '\0'); /* XXX str_chop(&msg); ??? */ } else { STR_ZAP(&opts); /* do not STR_FREE() any previous one! */ } /* XXX skip whitespace up to the first ';'??? */ /* get (optional) message: "/RE/options; message" */ msglen = end_ent - p - 1; if (*p++ == ';' && msglen > 0) { STR_INIT(&msg); /* do not re-use */ STR_NCAT(&msg, p, msglen); STR_NEXT(&msg, '\0'); /* XXX str_chop(&msg); ??? */ } else { STR_ZAP(&msg); /* do not STR_FREE() any previous one! */ } if (!(re_info = compile_pcre_expr(STR(&exp), delim_c, STR(&opts), STR(&msg), matchresult, errorp))) { STR_FREE(&exp); STR_FREE(&opts); STR_FREE(&msg); free_pcre_list(nl); return NULL; } nl = add_voidplist(nl, (char *) re_info, free_pcre_info); /* continue with next pattern */ if (*end_ent == '\0') { break; /* if there is one */ } } /* finally reverse the list so tests happen in the stated order */ /* XXX could implement and use append_voidplist() above to avoid this */ new = NULL; for (cur = nl; cur; cur = next) { next = cur->succ; cur->succ = new; new = cur; } return new; } static pcre_info_t * compile_pcre_expr(exp, delim_c, opts, msg, matchresult, errorp) char *exp; int delim_c; char *opts; char *msg; int matchresult; char **errorp; { pcre_info_t *re_info; int capturecount; const char *errtxt; int err_offset; int rc; re_info = (pcre_info_t *) xmalloc(sizeof(*re_info)); re_info->re = exp; re_info->delim_c = delim_c; re_info->matchresult = matchresult; re_info->flags = 0; if (opts) { const char *o; re_info->opts = opts; for (o = re_info->opts; *o; o++) { switch (*o) { case 'A': re_info->flags |= PCRE_ANCHORED; continue; case 'E': /* XXX PHP-4.x uses 'D', maybe we could use '$'? */ re_info->flags |= PCRE_DOLLAR_ENDONLY; continue; case 'U': re_info->flags |= PCRE_UNGREEDY; continue; case 'X': re_info->flags |= PCRE_EXTRA; continue; case 'i': re_info->flags |= PCRE_CASELESS; continue; case 'm': re_info->flags |= PCRE_MULTILINE; continue; case 's': re_info->flags |= PCRE_DOTALL; continue; case 'x': re_info->flags |= PCRE_EXTENDED; continue; } if (! isspace((int) *o)) { *errorp = xprintf("invalid option '%c' for RE %c%v%c%v", *o, delim_c, re_info->re, delim_c, re_info->opts); xfree(re_info->re); xfree(re_info->opts); xfree((char *) re_info); return NULL; } } } else { re_info->opts = NULL; } /* get any accompanying message into re_info->msg */ re_info->msg = msg; /* note: we use %s here so we see the RE in it's "natural" form. */ DEBUG4(DBG_MAIN_HI, "compile_pcre_expr(): compiling RE %c%v%c%v\n", delim_c, re_info->re, delim_c, re_info->opts ? re_info->opts : ""); if (re_info->msg) { DEBUG1(DBG_MAIN_HI, "\twith message '%v'\n", re_info->msg); } re_info->code = pcre_compile(re_info->re, re_info->flags, &errtxt, &err_offset, (const unsigned char *) NULL); if (! re_info->code) { *errorp = xprintf("error in regex %c%v%c%v at offset %d: %s", delim_c, re_info->re, delim_c, re_info->opts ? re_info->opts : "", err_offset, errtxt); xfree((char *) re_info); return NULL; } errtxt = NULL; re_info->hints = pcre_study(re_info->code, 0, &errtxt); if (re_info->hints) { DEBUG1(DBG_MAIN_HI, "compile_pcre_expr(): pcre_study() returned optimisation stuff: match_limit=%lu\n", re_info->hints->match_limit); } if (!re_info->hints && errtxt) { *errorp = xprintf("error while studying RE %c%v%c%v: %s", delim_c, re_info->re, delim_c, re_info->opts ? re_info->opts : "", errtxt); free_pcre_info(re_info); return NULL; } if ((rc = pcre_fullinfo(re_info->code, re_info->hints, PCRE_INFO_CAPTURECOUNT, &capturecount)) != 0) { *errorp = xprintf("pcre_fullinfo(CAPTURECOUNT) failed [%d] for RE %c%v%c%v", rc, delim_c, re_info->re, delim_c, re_info->opts ? re_info->opts : ""); free_pcre_info(re_info); return NULL; } /* we always capture at least one string -- the whole match... */ capturecount++; re_info->ovecsize = capturecount * 3; /* table is 3 wide */ return re_info; } /* * free_pcre_info - free all storage pointed to by a pcre_info_t pointer. * * Note a pointer to this function is passed to add_voidplist() for later use * by free_voidplist(), thus its "odd" signature using void*. */ void free_pcre_info(val) void *val; { register pcre_info_t *re_info = val; /* * XXX does pcre_study() return newly allocated storage which we point to * using re_info->hints? There's no hint either way in the pcreapi(3) * manual.... */ xfree((char *) re_info->code); xfree((char *) re_info); } /* * match_pcre_list - match the string sp against a pre-compiled list of PCRE REs * * RETURNS: MATCH_MATCHED if matched, else MATCH_NOMATCH, and MATCH_FAIL if * anything goes wrong. * * May return MATCH_NOMATCH immediately if a negated expression matches, thus * terminating the search. * * If a match is found, even a negated match, and if re_info->msg is not NULL, * then the *reasonp will be set to point to a copy of its value, after * sub-string substitutions have been performed on it if do_subs is non-zero. * * If no match is found and no errors encountered, or if re_info->msg is NULL, * then *reasonp will also be set to NULL. * * XXX Maybe "sp" should just be a "struct str *" and get rid of len? */ int match_pcre_list(sp, len, pcre_list, do_subs, reasonp) char *sp; /* string we are matching */ size_t len; /* length of string */ voidplist_t *pcre_list; /* pre-compiled RE list */ int do_subs; /* perform substitutions on message? */ char **reasonp; /* optional pointer for returned message string */ { voidplist_t *pat; char *no_reason; if (!reasonp) { reasonp = &no_reason; } *reasonp = NULL; for (pat = pcre_list; pat; pat = pat->succ) { pcre_info_t *re_info = (pcre_info_t *) pat->storage; switch (match_pcre_expr(sp, len, re_info, do_subs, reasonp)) { case MATCH_NOMATCH: continue; case MATCH_MATCHED: return re_info->matchresult; case MATCH_FAIL: return MATCH_FAIL; #if 0 /* #ifndef NDEBUG */ default: assert(FALSE); #endif } } return MATCH_NOMATCH; } /* * match_pcre_expr() - match an RE compiled by compile_pcre_expr() * * RETURNS: MATCH_MATCHED if matched, else MATCH_NOMATCH, and MATCH_FAIL if * anything goes wrong. * * Note re_info->matchresult is ignored -- it is only useful in the context of * a list of REs and so it can only sensibly be made use of by the caller. * * If a match is found and if re_info->msg is not NULL, then the *reasonp will * be set to point to a copy of its value, after sub-string substitutions have * been performed on it if do_subs is non-zero. * * If returning MATCH_FAIL then *reasonp will point to an error message; * * XXX Maybe "sp" should just be a "struct str *" and get rid of len? */ static int match_pcre_expr(sp, len, re_info, do_subs, reasonp) char *sp; /* string we are matching */ size_t len; /* length of string */ pcre_info_t *re_info; /* RE stuff [result of compile_pcre_expr()] */ int do_subs; /* perform substitutions on message? */ char **reasonp; /* pointer for returned message string */ { int rc; int *ovec; DEBUG8(DBG_MAIN_MID, "match_pcre_expr(): checking RE %s%c%v%c%v\n against '%V'%s\n", re_info->matchresult == MATCH_NOMATCH ? "! " : "", re_info->delim_c, re_info->re, re_info->delim_c, re_info->opts ? re_info->opts : "", MIN(len, 60), sp, (len >= 60) ? "[....]" : ""); ovec = (int *) xmalloc((size_t) re_info->ovecsize * sizeof(int)); rc = pcre_exec(re_info->code, re_info->hints, sp, (int) len, 0, 0, ovec, re_info->ovecsize); if (rc == PCRE_ERROR_NOMATCH) { xfree((char *) ovec); return MATCH_NOMATCH; } if (rc <= 0) { switch (rc) { case 0: *reasonp = xprintf("pcre_exec() encountered too many subexpressions in RE %c%v%c%v (%d expected)", re_info->delim_c, re_info->re, re_info->delim_c, re_info->opts ? re_info->opts : "", ((re_info->ovecsize / 3) - 1)); break; case PCRE_ERROR_NULL: /* XXX should we print 'sp' here too? only if really null? */ *reasonp = xprintf("bad pcre_exec() parameter for RE %c%v%c%v", re_info->delim_c, re_info->re, re_info->delim_c, re_info->opts ? re_info->opts : ""); break; case PCRE_ERROR_NOMEMORY: /* XXX impossible */ *reasonp = xprintf("pcre_exec() ran out of memory while matching RE %c%v%c%v", re_info->delim_c, re_info->re, re_info->delim_c, re_info->opts ? re_info->opts : ""); break; case PCRE_ERROR_MATCHLIMIT: { int ml; (void) pcre_config(PCRE_CONFIG_MATCH_LIMIT, (void *) &ml); *reasonp = xprintf("pcre_exec() hit recursion or backtracking limit using RE %c%v%c%v (%smatch limit = %d)", re_info->delim_c, re_info->re, re_info->delim_c, re_info->opts ? re_info->opts : "", re_info->hints ? "using default " : "", re_info->hints ? re_info->hints->match_limit : ml); break; } case PCRE_ERROR_BADMAGIC: case PCRE_ERROR_UNKNOWN_NODE: *reasonp = xprintf("pcre_exec() given corrupt compiled RE %c%v%c%v", re_info->delim_c, re_info->re, re_info->delim_c, re_info->opts ? re_info->opts : ""); break; default: *reasonp = xprintf("unexpected pcre_exec() error code #%d using RE %c%v%c%v", rc, re_info->delim_c, re_info->re, re_info->delim_c, re_info->opts ? re_info->opts : ""); break; } DEBUG1(DBG_MAIN_LO, "match_pcre_expr(): %s\n", *reasonp); xfree((char *) ovec); return MATCH_FAIL; } DEBUG6(DBG_MAIN_HI, "match_pcre_expr(): matched RE %s%c%v%c%v\n\twith %d substrings\n", re_info->matchresult == MATCH_NOMATCH ? "! " : "", re_info->delim_c, re_info->re, re_info->delim_c, re_info->opts, rc); if (re_info->msg) { if (do_subs) { *reasonp = pcre_expand_msg(re_info, sp, ovec, rc); } else { *reasonp = xprintf("%v", re_info->msg); } } else { const char *matched = NULL; if ((rc = pcre_get_substring(sp, ovec, rc, 0, &matched)) < 0) { DEBUG5(DBG_MAIN_LO, "match_pcre_expr(): unexpected error #%d getting matched string for RE %c%v%c%v\n", rc, re_info->delim_c, re_info->re, re_info->delim_c, re_info->opts); matched = sp; } *reasonp = xprintf("RE %c%v%c%v matched '%v'", re_info->delim_c, re_info->re, re_info->delim_c, re_info->opts ? re_info->opts : "", matched); if (rc >= 0) { pcre_free_substring(matched); } } xfree((char *) ovec); return MATCH_MATCHED; } /* * Do Python-style variable substitution on the message associated with the RE * using values matched by the named subpatterns. */ /* ARGSUSED */ static char * pcre_expand_msg(re_info, sp, ovec, nss) pcre_info_t *re_info; char *sp; /* len not needed for pcre_get_*substring() */ int *ovec; int nss; { struct str out; struct str *op = &out; char *p; /* message index pointer */ char *q; /* digit pointer */ char *ep; /* message end pointer */ char *np; /* substring name */ const char *vp; /* substring value pointer */ int vlen; /* substring value length */ STR_INIT(op); for (p = re_info->msg; *p; p++) { if (*p == '$' || *p == '%') { p++; if (*p == '{') { p++; for (ep = p; *ep && *ep != '}' && (isalnum(*ep) || *ep == '_'); ep++) { ; /* NO-OP */ } if (*ep != '}') { DEBUG3(DBG_MAIN_LO, "pcre_expand_msg(): unterminated variable expression: '${%S' in '%v'\n", (size_t) (ep - p), p, re_info->msg); /* just put back the expression */ STR_CAT(op, "${"); STR_NCAT(op, p, (size_t) (ep - p)); STR_NEXT(op, '}'); p = ep; continue; } } else { for (ep = p; *ep && (isalnum(*ep) || *ep == '_'); ep++) { ; /* NO-OP */ } } np = strncpy(xmalloc((size_t) (ep - p + 1)), p, (size_t) (ep - p)); np[ep - p] = '\0'; for (q = np; isdigit(*q); q++) { ; /* NO-OP */ } vp = NULL; if (*q == '\0') { vlen = pcre_get_substring(sp, ovec, nss, atoi(np), &vp); } else { vlen = pcre_get_named_substring(re_info->code, sp, ovec, nss, np, &vp); } if (vlen < 0) { switch (vlen) { case PCRE_ERROR_NOSUBSTRING: DEBUG2(DBG_MAIN_LO, "pcre_expand_msg(): no valid substring %s '${%s}'\n", *q ? "named" : "numbered", np); break; default: DEBUG7(DBG_MAIN_LO, "pcre_expand_msg(): unexpected error #%d getting substring %s '${%s}' for RE %c%v%c%v\n", vlen, *q ? "named" : "numbered", np, re_info->delim_c, re_info->re, re_info->delim_c, re_info->opts); break; } /* substitute the empty string... */ } else { str_printf(op, "%V", (size_t) vlen, vp); pcre_free_substring(vp); } xfree(np); p = (*ep == '}') ? ep : (ep - 1); } else { STR_NEXT(op, *p); } } STR_NEXT(op, '\0'); return STR(op); } void free_pcre_list(re_list) voidplist_t *re_list; { voidplist_t *pat; voidplist_t *next; for (pat = re_list; pat; pat = next) { pcre_info_t *re_info = (pcre_info_t *) pat->storage; next = pat->succ; xfree((char *) re_info->re); xfree((char *) re_info->code); if (re_info->opts) { xfree((char *) re_info->opts); } if (re_info->msg) { xfree((char *) re_info->msg); } xfree((char *) re_info); xfree((char *) pat); } return; } /* * turn re_list back into super-safe canonical pretty-printed input form */ char * format_pcre_list(re_list) voidplist_t *re_list; { struct str fmt; struct str *fmtp = &fmt; voidplist_t *pat; STR_INIT(fmtp); STR_CAT(fmtp, "\"\n\t:"); for (pat = re_list; pat; pat = pat->succ) { pcre_info_t *re_info = (pcre_info_t *) pat->storage; STR_NEXT(fmtp, re_info->delim_c); /* XXX we could wrap with an escaped newline at the special character * nearest the 80'th column.... */ str_printf(fmtp, "%v", re_info->re); STR_NEXT(fmtp, re_info->delim_c); if (re_info->opts) { STR_CAT(fmtp, re_info->opts); } STR_CAT(fmtp, ";\\\n\t\t"); if (re_info->msg) { /* XXX we could wrap with an escaped newline at the whitespace * nearest the 80'th column.... */ str_printf(fmtp, "%v", re_info->msg); } STR_CAT(fmtp, "\\\n\t:"); } STR_CAT(fmtp, "\""); return STR(fmtp); } /* * trusted_invoker -- check if the real_uid is the UID of a user in * trusted_users, or prog_egid is the GID of a group in trusted_groups. */ int trusted_invoker() { char *tm = NULL; int just_trust = TRUE; /* set false if trusted_users or trusted_groups are set */ if (trusted_users && trusted_users[0]) { just_trust = FALSE; /* trusted_users is a colon separated list of user names */ for (tm = strcolon(trusted_users); tm; tm = strcolon((char *) NULL)) { struct passwd *pw = getpwbyname(FALSE, tm); if (pw && real_uid == pw->pw_uid) { /* only trust the _real_ user */ DEBUG1(DBG_MAIN_HI, "invoker is a trusted user: %s\n", pw->pw_name); break; } } } if (tm == NULL && trusted_groups && trusted_groups[0]) { just_trust = FALSE; /* trusted_groups is a colon separated list of group names */ for (tm = strcolon(trusted_groups); tm; tm = strcolon((char *) NULL)) { struct group *gr = getgrbyname(tm); if (gr && prog_egid == gr->gr_gid) { /* trust the _effective_ group */ DEBUG1(DBG_MAIN_MID, "effective UID is trusted group: %s\n", gr->gr_name); break; } } } if (tm == NULL && !just_trust) { return FALSE; } if (just_trust) { DEBUG1(DBG_MAIN_LO, "invoker is an unverified user %s\n", local_sender); } return TRUE; } /* * Local Variables: * c-file-style: "smail" * End: */