blob: 8d3a53c57a04070ab55e9fef9b6cb81fa87ff2ef [file] [log] [blame]
Brian Waters13d96012017-12-08 16:53:31 -06001/*********************************************************************************************************
2* Software License Agreement (BSD License) *
3* Author: Sebastien Decugis <sdecugis@freediameter.net> *
4* *
5* Copyright (c) 2013, WIDE Project and NICT *
6* All rights reserved. *
7* *
8* Redistribution and use of this software in source and binary forms, with or without modification, are *
9* permitted provided that the following conditions are met: *
10* *
11* * Redistributions of source code must retain the above *
12* copyright notice, this list of conditions and the *
13* following disclaimer. *
14* *
15* * Redistributions in binary form must reproduce the above *
16* copyright notice, this list of conditions and the *
17* following disclaimer in the documentation and/or other *
18* materials provided with the distribution. *
19* *
20* * Neither the name of the WIDE Project or NICT nor the *
21* names of its contributors may be used to endorse or *
22* promote products derived from this software without *
23* specific prior written permission of WIDE Project and *
24* NICT. *
25* *
26* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED *
27* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A *
28* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR *
29* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT *
30* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS *
31* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR *
32* TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF *
33* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *
34*********************************************************************************************************/
35
36#include "rt_default.h"
37
38/* The regular expressions header */
39#include <regex.h>
40
41/* We will search for each candidate peer all the rules that are defined, and check which one applies to the message
42 * Therefore our repository is organized hierarchicaly.
43 * At the top level, we have two lists of TARGETS (one for IDENTITY, one for REALM), ordered as follow:
44 * - first, the TARGETS defined with a regular expression. We will try matching all regexp to all candidates in the list.
45 * - then, the TARGETS defined by a plain text. We don't have to compare the whole list to each candidate since the list is ordered.
46 *
47 * Under each TARGET element, we have the list of RULES that are defined for this target, ordered by CRITERIA type, then is_regex, then string value.
48 *
49 * Note: Except during configuration parsing and module termination, the lists are only ever accessed read-only, so we do not need a lock.
50 */
51
52/* Structure to hold the data that is used for matching. */
53struct match_data {
54 int is_regex; /* determines how the string is matched */
55 char *plain; /* match this value with strcasecmp if is_regex is false. The string is allocated by the lex tokenizer, must be freed at the end. */
56 regex_t preg; /* match with regexec if is_regex is true. regfree must be called at the end. A copy of the original string is anyway saved in plain. */
57};
58
59/* The sentinels for the TARGET lists */
60static struct fd_list TARGETS[RTD_TAR_MAX];
61
62/* Structure of a TARGET element */
63struct target {
64 struct fd_list chain; /* link in the top-level list */
65 struct match_data md; /* the data to determine if the current candidate matches this element */
66 struct fd_list rules[RTD_CRI_MAX]; /* Sentinels for the lists of rules applying to this target. One list per rtd_crit_type */
67 /* note : we do not need the rtd_targ_type here, it is implied by the root of the list this target element is attached to */
68};
69
70/* Structure of a RULE element */
71struct rule {
72 struct fd_list chain; /* link in the parent target list */
73 struct match_data md; /* the data that the criteria must match, -- ignored for RTD_CRI_ALL */
74 int score; /* The score added to the candidate, if the message matches this criteria */
75 /* The type of rule depends on the sentinel */
76};
77
78/*********************************************************************/
79
80/* Compile a regular expression pattern */
81static int compile_regex( regex_t * preg, char * str )
82{
83 int err;
84
85 /* Compile the regular expression */
86 err = regcomp(preg, str, REG_EXTENDED | REG_NOSUB);
87 if (err != 0) {
88 char * buf;
89 size_t bl;
90
91 /* Error while compiling the regex */
92 TRACE_DEBUG(INFO, "Error while compiling the regular expression '%s':", str);
93
94 /* Get the error message size */
95 bl = regerror(err, preg, NULL, 0);
96
97 /* Alloc the buffer for error message */
98 CHECK_MALLOC( buf = malloc(bl) );
99
100 /* Get the error message content */
101 regerror(err, preg, buf, bl);
102 TRACE_DEBUG(INFO, "\t%s", buf);
103
104 /* Free the buffer, return the error */
105 free(buf);
106 return EINVAL;
107 }
108
109 return 0;
110}
111
112/* Create a target item and initialize its content */
113static struct target * new_target(char * str, int regex)
114{
115 int i;
116 struct target *new = NULL;
117 CHECK_MALLOC_DO( new = malloc(sizeof(struct target)), return NULL );
118 memset(new, 0, sizeof(struct target));
119
120 fd_list_init(&new->chain, new);
121 new->md.plain = str;
122 new->md.is_regex = regex;
123 if (regex) {
124 CHECK_FCT_DO( compile_regex(&new->md.preg, str),
125 {
126 free(new);
127 return NULL;
128 } );
129 }
130 for (i = 0; i < RTD_CRI_MAX; i++) {
131 fd_list_init(&new->rules[i], new);
132 }
133 return new;
134}
135
136/* Create a rule item and initialize its content; return NULL in case of error */
137static struct rule * new_rule(char * str, int regex, int score)
138{
139 struct rule *new = NULL;
140 CHECK_MALLOC_DO( new = malloc(sizeof(struct rule)), return NULL );
141 memset(new, 0, sizeof(struct rule));
142
143 fd_list_init(&new->chain, new);
144 new->md.plain = str;
145 new->md.is_regex = regex;
146 if (regex) {
147 CHECK_FCT_DO( compile_regex(&new->md.preg, str),
148 {
149 free(new);
150 return NULL;
151 } );
152 }
153 new->score = score;
154 return new;
155}
156
157/* Debug functions */
158static void dump_rule(int indent, struct rule * rule)
159{
160 fd_log_debug("%*s%s%s%s += %d",
161 indent, "",
162 rule->md.is_regex ? "[" : "'",
163 rule->md.plain,
164 rule->md.is_regex ? "]" : "'",
165 rule->score);
166}
167static void dump_target(int indent, struct target * target)
168{
169 int i;
170 fd_log_debug("%*s%s%s%s :",
171 indent, "",
172 target->md.is_regex ? "[" : "'",
173 target->md.plain ?: "(empty)",
174 target->md.is_regex ? "]" : "'");
175 for (i = 0; i < RTD_CRI_MAX; i++) {
176 if (! FD_IS_LIST_EMPTY(&target->rules[i])) {
177 struct fd_list * li;
178 fd_log_debug("%*s rules[%d]:",
179 indent, "", i);
180 for (li = target->rules[i].next; li != &target->rules[i]; li = li->next) {
181 dump_rule(indent + 3, (struct rule *)li);
182 }
183 }
184 }
185}
186
187static void clear_md(struct match_data * md)
188{
189 /* delete the string */
190 if (md->plain) {
191 free(md->plain);
192 md->plain = NULL;
193 }
194
195 /* delete the preg if needed */
196 if (md->is_regex) {
197 regfree(&md->preg);
198 md->is_regex = 0;
199 }
200}
201
202/* Destroy a rule item */
203static void del_rule(struct rule * del)
204{
205 /* Unlink this rule */
206 fd_list_unlink(&del->chain);
207
208 /* Delete the match data */
209 clear_md(&del->md);
210
211 free(del);
212}
213
214/* Destroy a target item, and all its rules */
215static void del_target(struct target * del)
216{
217 int i;
218
219 /* Unlink this target */
220 fd_list_unlink(&del->chain);
221
222 /* Delete the match data */
223 clear_md(&del->md);
224
225 /* Delete the children rules */
226 for (i = 0; i < RTD_CRI_MAX; i++) {
227 while (! FD_IS_LIST_EMPTY(&del->rules[i]) ) {
228 del_rule((struct rule *)(del->rules[i].next));
229 }
230 }
231
232 free(del);
233}
234
235
236/* Compare a string with a match_data value. *res contains the result of the comparison (always >0 for regex non-match situations) */
237static int compare_match(char * str, size_t len, struct match_data * md, int * res)
238{
239 int err;
240
241 CHECK_PARAMS( str && md && res );
242
243 /* Plain strings: we compare with strncasecmp */
244 if (md->is_regex == 0) {
245 *res = strncasecmp(str, md->plain, len);
246 return 0;
247 }
248
249 /* Regexp */
250 *res = 1;
251
252#ifdef HAVE_REG_STARTEND
253 {
254 regmatch_t pmatch[1];
255 memset(pmatch, 0, sizeof(pmatch));
256 pmatch[0].rm_so = 0;
257 pmatch[0].rm_eo = len;
258 err = regexec(&md->preg, str, 0, pmatch, REG_STARTEND);
259 }
260#else /* HAVE_REG_STARTEND */
261 {
262 /* We have to create a copy of the string in this case */
263 char *mystrcpy;
264 CHECK_MALLOC( mystrcpy = os0dup(str, len) );
265 err = regexec(&md->preg, mystrcpy, 0, NULL, 0);
266 free(mystrcpy);
267 }
268#endif /* HAVE_REG_STARTEND */
269
270 /* Now check the result */
271 if (err == 0) {
272 /* We have a match */
273 *res = 0;
274 return 0;
275 }
276
277 if (err == REG_NOMATCH) {
278 *res = 1;
279 return 0;
280 }
281
282 /* In other cases, we have an error */
283 {
284 char * buf;
285 size_t bl;
286
287 /* Error while compiling the regex */
288 TRACE_DEBUG(INFO, "Error while executing the regular expression '%s':", md->plain);
289
290 /* Get the error message size */
291 bl = regerror(err, &md->preg, NULL, 0);
292
293 /* Alloc the buffer for error message */
294 CHECK_MALLOC( buf = malloc(bl) );
295
296 /* Get the error message content */
297 regerror(err, &md->preg, buf, bl);
298 TRACE_DEBUG(INFO, "\t%s", buf);
299
300 /* Free the buffer, return the error */
301 free(buf);
302 }
303
304 return (err == REG_ESPACE) ? ENOMEM : EINVAL;
305}
306
307/* Search in list (targets or rules) the next matching item for octet string str(len). Returned in next_match, or *next_match == NULL if no more match. Re-enter with same next_match for the next one. */
308static int get_next_match(struct fd_list * list, char * str, size_t len, struct fd_list ** next_match)
309{
310 struct fd_list * li;
311
312 TRACE_ENTRY("%p %p %zd %p", list, str, len, next_match);
313 CHECK_PARAMS(list && str && len && next_match);
314
315 if (*next_match)
316 li = (*next_match)->next;
317 else
318 li = list->next;
319
320 /* Initialize the return value */
321 *next_match = NULL;
322
323 for ( ; li != list; li = li->next) {
324 int cmp;
325 struct {
326 struct fd_list chain;
327 struct match_data md;
328 } * next_item = (void *)li;
329
330 /* Check if the string matches this next item */
331 CHECK_FCT( compare_match(str, len, &next_item->md, &cmp) );
332
333 if (cmp == 0) {
334 /* matched! */
335 *next_match = li;
336 return 0;
337 }
338
339 if (cmp < 0) /* we can stop searching */
340 break;
341 }
342
343 /* We're done with the list */
344 return 0;
345}
346
347static struct dict_object * AVP_MODELS[RTD_CRI_MAX];
348
349/*********************************************************************/
350
351/* Prepare the module */
352int rtd_init(void)
353{
354 int i;
355
356 TRACE_ENTRY();
357
358 for (i = 0; i < RTD_TAR_MAX; i++) {
359 fd_list_init(&TARGETS[i], NULL);
360 }
361
362 for (i = 1; i < RTD_CRI_MAX; i++) {
363 switch (i) {
364 case RTD_CRI_OH:
365 CHECK_FCT( fd_dict_search ( fd_g_config->cnf_dict, DICT_AVP, AVP_BY_NAME, "Origin-Host", &AVP_MODELS[i], ENOENT ));
366 break;
367 case RTD_CRI_OR:
368 CHECK_FCT( fd_dict_search ( fd_g_config->cnf_dict, DICT_AVP, AVP_BY_NAME, "Origin-Realm", &AVP_MODELS[i], ENOENT ));
369 break;
370 case RTD_CRI_DH:
371 CHECK_FCT( fd_dict_search ( fd_g_config->cnf_dict, DICT_AVP, AVP_BY_NAME, "Destination-Host", &AVP_MODELS[i], ENOENT ));
372 break;
373 case RTD_CRI_DR:
374 CHECK_FCT( fd_dict_search ( fd_g_config->cnf_dict, DICT_AVP, AVP_BY_NAME, "Destination-Realm", &AVP_MODELS[i], ENOENT ));
375 break;
376 case RTD_CRI_UN:
377 CHECK_FCT( fd_dict_search ( fd_g_config->cnf_dict, DICT_AVP, AVP_BY_NAME, "User-Name", &AVP_MODELS[i], ENOENT ));
378 break;
379 case RTD_CRI_SI:
380 CHECK_FCT( fd_dict_search ( fd_g_config->cnf_dict, DICT_AVP, AVP_BY_NAME, "Session-Id", &AVP_MODELS[i], ENOENT ));
381 break;
382 default:
383 TRACE_DEBUG(INFO, "Missing definition in extension initializer");
384 ASSERT( 0 );
385 return EINVAL;
386 }
387 }
388
389 return 0;
390}
391
392/* Destroy the module's data */
393void rtd_fini(void)
394{
395 int i;
396
397 TRACE_ENTRY();
398
399 for (i = 0; i < RTD_TAR_MAX; i++) {
400 while (!FD_IS_LIST_EMPTY(&TARGETS[i])) {
401 del_target((struct target *) TARGETS[i].next);
402 }
403 }
404
405}
406
407/* Add a new rule in the repository. this is called when the configuration file is being parsed */
408int rtd_add(enum rtd_crit_type ct, char * criteria, enum rtd_targ_type tt, char * target, int score, int flags)
409{
410 struct fd_list * target_suiv = NULL;
411 struct fd_list * rule_suiv = NULL;
412 struct target * trg = NULL;
413 struct rule * rul = NULL;
414
415 TRACE_ENTRY("%d %p %d %p %d %x", ct, criteria, tt, target, score, flags);
416 CHECK_PARAMS((ct < RTD_CRI_MAX) && ((ct == RTD_CRI_ALL) || criteria) && (tt < RTD_TAR_MAX) && target);
417
418 /* First, search in the TARGET list if we already have this target */
419 for (target_suiv = TARGETS[tt].next; target_suiv != &TARGETS[tt]; target_suiv = target_suiv->next) {
420 int cmp;
421 struct target * cur = (struct target *)target_suiv;
422
423 if (flags & RTD_TARG_REG) {
424 /* We are adding a regexp, it is saved in the list before the plain expressions */
425 if (cur->md.is_regex == 0)
426 break;
427 } else {
428 /* We are adding a plain expression, save it after all regexps */
429 if (cur->md.is_regex != 0)
430 continue;
431 }
432
433 /* At this point, the type is the same, so compare the plain string value */
434 cmp = strcmp(cur->md.plain, target);
435 if (cmp < 0)
436 continue;
437
438 if (cmp == 0) /* We already have a target with the same string */
439 trg = cur;
440
441 break;
442 }
443
444 if (trg) {
445 /* Ok, we can free the target string, we will use the previously allocated one */
446 free(target);
447 } else {
448 CHECK_MALLOC( trg = new_target(target, flags & RTD_TARG_REG) );
449 fd_list_insert_before( target_suiv, &trg->chain );
450 }
451
452 /* Now, search for the rule position in this target's list */
453 if (ct == RTD_CRI_ALL) {
454 /* Special case: we don't have a criteria -- always create a rule element */
455 CHECK_MALLOC( rul = new_rule(NULL, 0, score) );
456 fd_list_insert_before( &trg->rules[RTD_CRI_ALL], &rul->chain );
457 } else {
458 for (rule_suiv = trg->rules[ct].next; rule_suiv != &trg->rules[ct]; rule_suiv = rule_suiv->next) {
459 int cmp;
460 struct rule * cur = (struct rule *)rule_suiv;
461
462 if (flags & RTD_CRIT_REG) {
463 /* We are adding a regexp, it is saved in the list before the plain expressions */
464 if (cur->md.is_regex == 0)
465 break;
466 } else {
467 /* We are adding a plain expression, save it after all regexps */
468 if (cur->md.is_regex != 0)
469 continue;
470 }
471
472 /* At this point, the type is the same, so compare the plain string value */
473 cmp = strcmp(cur->md.plain, criteria);
474 if (cmp < 0)
475 continue;
476
477 if (cmp == 0) /* We already have a target with the same string */
478 rul = cur;
479
480 break;
481 }
482
483 if (rul) {
484 /* Ok, we can free the target string, we will use the previously allocated one */
485 free(criteria);
486 TRACE_DEBUG(INFO, "Warning: duplicate rule (%s : %s) found, merging score...", rul->md.plain, trg->md.plain);
487 rul->score += score;
488 } else {
489 CHECK_MALLOC( rul = new_rule(criteria, flags & RTD_CRIT_REG, score) );
490 fd_list_insert_before( rule_suiv, &rul->chain );
491 }
492 }
493
494 return 0;
495}
496
497/* Check if a message and list of eligible candidate match any of our rules, and update its score according to it. */
498int rtd_process( struct msg * msg, struct fd_list * candidates )
499{
500 struct fd_list * li;
501 struct {
502 enum { NOT_RESOLVED_YET = 0, NOT_FOUND, FOUND } status;
503 union avp_value * avp;
504 } parsed_msg_avp[RTD_CRI_MAX];
505
506 TRACE_ENTRY("%p %p", msg, candidates);
507 CHECK_PARAMS(msg && candidates);
508
509 /* We delay looking for the AVPs in the message until we really need them. Another approach would be to parse the message once and save all needed AVPs. */
510 memset(parsed_msg_avp, 0, sizeof(parsed_msg_avp));
511
512 /* For each candidate in the list */
513 for (li = candidates->next; li != candidates; li = li->next) {
514 struct rtd_candidate * cand = (struct rtd_candidate *)li;
515 int i;
516 struct {
517 char * str;
518 size_t len;
519 } cand_data[RTD_TAR_MAX] = {
520 { cand->diamid, strlen(cand->diamid) },
521 { cand->realm, strlen(cand->realm) }
522 };
523
524 for (i = 0; i < RTD_TAR_MAX; i++) {
525 /* Search the next rule matching this candidate in the i-th target list */
526 struct target * target = NULL;
527
528 do {
529 int j;
530 struct fd_list * l;
531 struct rule * r;
532
533 CHECK_FCT ( get_next_match( &TARGETS[i], cand_data[i].str, cand_data[i].len, (void *)&target) );
534 if (!target)
535 break;
536
537 /* First, apply all rules of criteria RTD_CRI_ALL */
538 for ( l = target->rules[RTD_CRI_ALL].next; l != &target->rules[RTD_CRI_ALL]; l = l->next ) {
539 r = (struct rule *)l;
540 cand->score += r->score;
541 TRACE_DEBUG(ANNOYING, "Applied rule {'*' : '%s' += %d} to candidate '%s'", target->md.plain, r->score, cand->diamid);
542 }
543
544 /* The target is matching this candidate, check if there are additional rules criteria matching this message. */
545 for ( j = 1; j < RTD_CRI_MAX; j++ ) {
546 if ( ! FD_IS_LIST_EMPTY(&target->rules[j]) ) {
547 /* if needed, find the required data in the message */
548 if (parsed_msg_avp[j].status == NOT_RESOLVED_YET) {
549 struct avp * avp = NULL;
550 /* Search for the AVP in the message */
551 CHECK_FCT( fd_msg_search_avp ( msg, AVP_MODELS[j], &avp ) );
552 if (avp == NULL) {
553 parsed_msg_avp[j].status = NOT_FOUND;
554 } else {
555 struct avp_hdr * ahdr = NULL;
556 CHECK_FCT( fd_msg_avp_hdr ( avp, &ahdr ) );
557 if (ahdr->avp_value == NULL) {
558 /* This should not happen, but anyway let's just ignore it */
559 parsed_msg_avp[j].status = NOT_FOUND;
560 } else {
561 /* OK, we got the AVP */
562 parsed_msg_avp[j].status = FOUND;
563 parsed_msg_avp[j].avp = ahdr->avp_value;
564 }
565 }
566 }
567
568 /* If we did not find the data for these rules in the message, just skip the series */
569 if (parsed_msg_avp[j].status == NOT_FOUND) {
570 TRACE_DEBUG(ANNOYING, "Skipping series of rules %d of target '%s', criteria absent from the message", j, target->md.plain);
571 continue;
572 }
573
574 /* OK, we can now check if one of our rule's criteria match the message content */
575 r = NULL;
576 do {
577 CHECK_FCT ( get_next_match( &target->rules[j], (char *) /* is this cast safe? */ parsed_msg_avp[j].avp->os.data, parsed_msg_avp[j].avp->os.len, (void *)&r) );
578 if (!r)
579 break;
580
581 cand->score += r->score;
582 TRACE_DEBUG(ANNOYING, "Applied rule {'%s' : '%s' += %d} to candidate '%s'", r->md.plain, target->md.plain, r->score, cand->diamid);
583 } while (1);
584 }
585 }
586 } while (1);
587 }
588 }
589
590 return 0;
591}
592
593void rtd_dump(void)
594{
595 int i;
596 fd_log_debug("[rt_default] Dumping rules repository...");
597 for (i = 0; i < RTD_TAR_MAX; i++) {
598 if (!FD_IS_LIST_EMPTY( &TARGETS[i] )) {
599 struct fd_list * li;
600 fd_log_debug(" Targets list %d:", i);
601 for (li = TARGETS[i].next; li != &TARGETS[i]; li = li->next) {
602 dump_target(4, (struct target *)li);
603 }
604 }
605 }
606
607 fd_log_debug("[rt_default] End of dump");
608}