1 /*        $NetBSD: match_ops.c,v 1.3 2020/03/18 19:05:21 christos Exp $         */
2 
3 /*++
4 /* NAME
5 /*        match_ops 3
6 /* SUMMARY
7 /*        simple string or host pattern matching
8 /* SYNOPSIS
9 /*        #include <match_list.h>
10 /*
11 /*        int       match_string(list, string, pattern)
12 /*        MATCH_LIST *list;
13 /*        const char *string;
14 /*        const char *pattern;
15 /*
16 /*        int       match_hostname(list, name, pattern)
17 /*        MATCH_LIST *list;
18 /*        const char *name;
19 /*        const char *pattern;
20 /*
21 /*        int       match_hostaddr(list, addr, pattern)
22 /*        MATCH_LIST *list;
23 /*        const char *addr;
24 /*        const char *pattern;
25 /* DESCRIPTION
26 /*        This module implements simple string and host name or address
27 /*        matching. The matching process is case insensitive. If a pattern
28 /*        has the form type:name, table lookup is used instead of string
29 /*        or address comparison.
30 /*
31 /*        match_string() matches the string against the pattern, requiring
32 /*        an exact (case-insensitive) match. The flags argument is not used.
33 /*
34 /*        match_hostname() matches the host name when the hostname matches
35 /*        the pattern exactly, or when the pattern matches a parent domain
36 /*        of the named host. The flags argument specifies the bit-wise OR
37 /*        of zero or more of the following:
38 /* .IP MATCH_FLAG_PARENT
39 /*        The hostname pattern foo.com matches itself and any name below
40 /*        the domain foo.com. If this flag is cleared, foo.com matches itself
41 /*        only, and .foo.com matches any name below the domain foo.com.
42 /* .IP MATCH_FLAG_RETURN
43 /*        Log a warning, return "not found", and set list->error to
44 /*        a non-zero dictionary error code, instead of raising a fatal
45 /*        run-time error.
46 /* .RE
47 /*        Specify MATCH_FLAG_NONE to request none of the above.
48 /*
49 /*        match_hostaddr() matches a host address when the pattern is
50 /*        identical to the host address, or when the pattern is a net/mask
51 /*        that contains the address. The mask specifies the number of
52 /*        bits in the network part of the pattern. The flags argument is
53 /*        not used.
54 /* LICENSE
55 /* .ad
56 /* .fi
57 /*        The Secure Mailer license must be distributed with this software.
58 /* AUTHOR(S)
59 /*        Wietse Venema
60 /*        IBM T.J. Watson Research
61 /*        P.O. Box 704
62 /*        Yorktown Heights, NY 10598, USA
63 /*
64 /*        Wietse Venema
65 /*        Google, Inc.
66 /*        111 8th Avenue
67 /*        New York, NY 10011, USA
68 /*--*/
69 
70 /* System library. */
71 
72 #include <sys_defs.h>
73 #include <netinet/in.h>
74 #include <arpa/inet.h>
75 #include <string.h>
76 #include <stdlib.h>
77 
78 /* Utility library. */
79 
80 #include <msg.h>
81 #include <mymalloc.h>
82 #include <split_at.h>
83 #include <dict.h>
84 #include <match_list.h>
85 #include <stringops.h>
86 #include <cidr_match.h>
87 
88 #define MATCH_DICTIONARY(pattern) \
89     ((pattern)[0] != '[' && strchr((pattern), ':') != 0)
90 
91 /* match_error - return or raise fatal error */
92 
match_error(MATCH_LIST * list,const char * fmt,...)93 static int match_error(MATCH_LIST *list, const char *fmt,...)
94 {
95     VSTRING *buf = vstring_alloc(100);
96     va_list ap;
97 
98     /*
99      * Report, and maybe return.
100      */
101     va_start(ap, fmt);
102     vstring_vsprintf(buf, fmt, ap);
103     va_end(ap);
104     if (list->flags & MATCH_FLAG_RETURN) {
105           msg_warn("%s: %s", list->pname, vstring_str(buf));
106     } else {
107           msg_fatal("%s: %s", list->pname, vstring_str(buf));
108     }
109     vstring_free(buf);
110     return (0);
111 }
112 
113 /* match_string - match a string literal */
114 
match_string(MATCH_LIST * list,const char * string,const char * pattern)115 int     match_string(MATCH_LIST *list, const char *string, const char *pattern)
116 {
117     const char *myname = "match_string";
118     DICT   *dict;
119 
120     if (msg_verbose)
121           msg_info("%s: %s: %s ~? %s", myname, list->pname, string, pattern);
122 
123     /*
124      * Try dictionary lookup: exact match.
125      */
126     if (MATCH_DICTIONARY(pattern)) {
127           if ((dict = dict_handle(pattern)) == 0)
128               msg_panic("%s: unknown dictionary: %s", myname, pattern);
129           if (dict_get(dict, string) != 0)
130               return (1);
131           if ((list->error = dict->error) != 0)
132               return (match_error(list, "%s:%s: table lookup problem",
133                                         dict->type, dict->name));
134           return (0);
135     }
136 
137     /*
138      * Try an exact string match. Note that the string and pattern are
139      * already casefolded.
140      */
141     if (strcmp(string, pattern) == 0) {
142           return (1);
143     }
144 
145     /*
146      * No match found.
147      */
148     return (0);
149 }
150 
151 /* match_hostname - match a host by name */
152 
match_hostname(MATCH_LIST * list,const char * name,const char * pattern)153 int     match_hostname(MATCH_LIST *list, const char *name, const char *pattern)
154 {
155     const char *myname = "match_hostname";
156     const char *pd;
157     const char *entry;
158     const char *next;
159     int     match;
160     DICT   *dict;
161 
162     if (msg_verbose)
163           msg_info("%s: %s: %s ~? %s", myname, list->pname, name, pattern);
164 
165     /*
166      * Try dictionary lookup: exact match and parent domains.
167      *
168      * Don't look up parent domain substrings with regexp maps etc.
169      */
170     if (MATCH_DICTIONARY(pattern)) {
171           if ((dict = dict_handle(pattern)) == 0)
172               msg_panic("%s: unknown dictionary: %s", myname, pattern);
173           match = 0;
174           for (entry = name; *entry != 0; entry = next) {
175               if (entry == name || (dict->flags & DICT_FLAG_FIXED)) {
176                     match = (dict_get(dict, entry) != 0);
177                     if (msg_verbose > 1)
178                         msg_info("%s: %s: lookup %s:%s %s: %s",
179                                    myname, list->pname, dict->type, dict->name,
180                                    entry, match ? "found" : "notfound");
181                     if (match != 0)
182                         break;
183                     if ((list->error = dict->error) != 0)
184                         return (match_error(list, "%s:%s: table lookup problem",
185                                                   dict->type, dict->name));
186               }
187               if ((next = strchr(entry + 1, '.')) == 0)
188                     break;
189               if (list->flags & MATCH_FLAG_PARENT)
190                     next += 1;
191           }
192           return (match);
193     }
194 
195     /*
196      * Try an exact match with the host name. Note that the name and the
197      * pattern are already casefolded.
198      */
199     if (strcmp(name, pattern) == 0) {
200           return (1);
201     }
202 
203     /*
204      * See if the pattern is a parent domain of the hostname. Note that the
205      * name and the pattern are already casefolded.
206      */
207     else {
208           if (list->flags & MATCH_FLAG_PARENT) {
209               pd = name + strlen(name) - strlen(pattern);
210               if (pd > name && pd[-1] == '.' && strcmp(pd, pattern) == 0)
211                     return (1);
212           } else if (pattern[0] == '.') {
213               pd = name + strlen(name) - strlen(pattern);
214               if (pd > name && strcmp(pd, pattern) == 0)
215                     return (1);
216           }
217     }
218     return (0);
219 }
220 
221 /* match_hostaddr - match host by address */
222 
match_hostaddr(MATCH_LIST * list,const char * addr,const char * pattern)223 int     match_hostaddr(MATCH_LIST *list, const char *addr, const char *pattern)
224 {
225     const char *myname = "match_hostaddr";
226     char   *saved_patt;
227     CIDR_MATCH match_info;
228     DICT   *dict;
229     VSTRING *err;
230     int     rc;
231 
232     if (msg_verbose)
233           msg_info("%s: %s: %s ~? %s", myname, list->pname, addr, pattern);
234 
235 #define V4_ADDR_STRING_CHARS  "01234567890."
236 #define V6_ADDR_STRING_CHARS  V4_ADDR_STRING_CHARS "abcdefABCDEF:"
237 
238     if (addr[strspn(addr, V6_ADDR_STRING_CHARS)] != 0)
239           return (0);
240 
241     /*
242      * Try dictionary lookup. This can be case insensitive.
243      */
244     if (MATCH_DICTIONARY(pattern)) {
245           if ((dict = dict_handle(pattern)) == 0)
246               msg_panic("%s: unknown dictionary: %s", myname, pattern);
247           if (dict_get(dict, addr) != 0)
248               return (1);
249           if ((list->error = dict->error) != 0)
250               return (match_error(list, "%s:%s: table lookup problem",
251                                         dict->type, dict->name));
252           return (0);
253     }
254 
255     /*
256      * Try an exact match with the host address. Note that the address and
257      * pattern are already casefolded.
258      */
259     if (pattern[0] != '[') {
260           if (strcmp(addr, pattern) == 0)
261               return (1);
262     } else {
263           size_t  addr_len = strlen(addr);
264 
265           if (strncmp(addr, pattern + 1, addr_len) == 0
266               && strcmp(pattern + 1 + addr_len, "]") == 0)
267               return (1);
268     }
269 
270     /*
271      * Light-weight tests before we get into expensive operations.
272      *
273      * - Don't bother matching IPv4 against IPv6. Postfix transforms
274      * IPv4-in-IPv6 to native IPv4 form when IPv4 support is enabled in
275      * Postfix; if not, then Postfix has no business dealing with IPv4
276      * addresses anyway.
277      *
278      * - Don't bother unless the pattern is either an IPv6 address or net/mask.
279      *
280      * We can safely skip IPv4 address patterns because their form is
281      * unambiguous and they did not match in the strcmp() calls above.
282      *
283      * XXX We MUST skip (parent) domain names, which may appear in NAMADR_LIST
284      * input, to avoid triggering false cidr_match_parse() errors.
285      *
286      * The last two conditions below are for backwards compatibility with
287      * earlier Postfix versions: don't abort with fatal errors on junk that
288      * was silently ignored (principle of least astonishment).
289      */
290     if (!strchr(addr, ':') != !strchr(pattern, ':')
291           || pattern[strcspn(pattern, ":/")] == 0
292           || pattern[strspn(pattern, V4_ADDR_STRING_CHARS)] == 0
293           || pattern[strspn(pattern, V6_ADDR_STRING_CHARS "[]/")] != 0)
294           return (0);
295 
296     /*
297      * No escape from expensive operations: either we have a net/mask
298      * pattern, or we have an address that can have multiple valid
299      * representations (e.g., 0:0:0:0:0:0:0:1 versus ::1, etc.). The only way
300      * to find out if the address matches the pattern is to transform
301      * everything into to binary form, and to do the comparison there.
302      */
303     saved_patt = mystrdup(pattern);
304     err = cidr_match_parse(&match_info, saved_patt, CIDR_MATCH_TRUE,
305                                  (VSTRING *) 0);
306     myfree(saved_patt);
307     if (err != 0) {
308           list->error = DICT_ERR_RETRY;
309           rc = match_error(list, "%s", vstring_str(err));
310           vstring_free(err);
311           return (rc);
312     }
313     return (cidr_match_execute(&match_info, addr) != 0);
314 }
315