Backport rlm_cache merge fixes from master
[freeradius.git] / src / modules / rlm_cache / rlm_cache.c
1 /*
2  *   This program is is free software; you can redistribute it and/or modify
3  *   it under the terms of the GNU General Public License, version 2 if the
4  *   License as published by the Free Software Foundation.
5  *
6  *   This program is distributed in the hope that it will be useful,
7  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
8  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
9  *   GNU General Public License for more details.
10  *
11  *   You should have received a copy of the GNU General Public License
12  *   along with this program; if not, write to the Free Software
13  *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
14  */
15
16 /**
17  * $Id$
18  * @file rlm_cache.c
19  * @brief Cache values and merge them back into future requests.
20  *
21  * @copyright 2012-2013  The FreeRADIUS server project
22  */
23 RCSID("$Id$")
24
25 #include <freeradius-devel/radiusd.h>
26 #include <freeradius-devel/modules.h>
27 #include <freeradius-devel/modcall.h>
28 #include <freeradius-devel/heap.h>
29 #include <freeradius-devel/rad_assert.h>
30
31 /*
32  *      Define a structure for our module configuration.
33  *
34  *      These variables do not need to be in a structure, but it's
35  *      a lot cleaner to do so, and a pointer to the structure can
36  *      be used as the instance handle.
37  */
38 typedef struct rlm_cache_t {
39         char const              *xlat_name;
40         char const              *key;
41         uint32_t                ttl;
42         uint32_t                max_entries;
43         int32_t                 epoch;
44         bool                    stats;
45         CONF_SECTION            *cs;
46         rbtree_t                *cache;
47         fr_heap_t               *heap;
48
49         value_pair_map_t        *maps;  //!< Attribute map applied to users
50                                         //!< and profiles.
51 #ifdef HAVE_PTHREAD_H
52         pthread_mutex_t cache_mutex;
53 #endif
54 } rlm_cache_t;
55
56 typedef struct rlm_cache_entry_t {
57         char const      *key;
58         int             offset;
59         long long int   hits;
60         time_t          created;
61         time_t          expires;
62         VALUE_PAIR      *control;
63         VALUE_PAIR      *packet;
64         VALUE_PAIR      *reply;
65 } rlm_cache_entry_t;
66
67 #ifdef HAVE_PTHREAD_H
68 #define PTHREAD_MUTEX_LOCK pthread_mutex_lock
69 #define PTHREAD_MUTEX_UNLOCK pthread_mutex_unlock
70 #else
71 #define PTHREAD_MUTEX_LOCK(_x)
72 #define PTHREAD_MUTEX_UNLOCK(_x)
73 #endif
74
75 #define MAX_ATTRMAP     128
76
77 /*
78  *      A mapping of configuration file names to internal variables.
79  *
80  *      Note that the string is dynamically allocated, so it MUST
81  *      be freed.  When the configuration file parse re-reads the string,
82  *      it free's the old one, and strdup's the new one, placing the pointer
83  *      to the strdup'd string into 'config.string'.  This gets around
84  *      buffer over-flows.
85  */
86 static const CONF_PARSER module_config[] = {
87         { "key", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_REQUIRED | PW_TYPE_XLAT, rlm_cache_t, key), NULL },
88         { "ttl", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_cache_t, ttl), "500" },
89         { "max_entries", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_cache_t, max_entries), "16384" },
90
91         /* Should be a type which matches time_t, @fixme before 2038 */
92         { "epoch", FR_CONF_OFFSET(PW_TYPE_SIGNED, rlm_cache_t, epoch), "0" },
93         { "add_stats", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_cache_t, stats), "no" },
94
95         { NULL, -1, 0, NULL, NULL }             /* end the list */
96 };
97
98
99 /*
100  *      Compare two entries by key.  There may only be one entry with
101  *      the same key.
102  */
103 static int cache_entry_cmp(void const *one, void const *two)
104 {
105         rlm_cache_entry_t const *a = one;
106         rlm_cache_entry_t const *b = two;
107
108         return strcmp(a->key, b->key);
109 }
110
111 static void cache_entry_free(void *data)
112 {
113         rlm_cache_entry_t *c = data;
114
115         pairfree(&c->control);
116         pairfree(&c->packet);
117         pairfree(&c->reply);
118
119         talloc_free(c);
120 }
121
122 /*
123  *      Compare two entries by expiry time.  There may be multiple
124  *      entries with the same expiry time.
125  */
126 static int cache_heap_cmp(void const *one, void const *two)
127 {
128         rlm_cache_entry_t const *a = one;
129         rlm_cache_entry_t const *b = two;
130
131         if (a->expires < b->expires) return -1;
132         if (a->expires > b->expires) return +1;
133
134         return 0;
135 }
136
137 /*
138  *      Merge a cached entry into a REQUEST.
139  */
140 static void CC_HINT(nonnull) cache_merge(rlm_cache_t *inst, REQUEST *request, rlm_cache_entry_t *c)
141 {
142         VALUE_PAIR *vp;
143
144         vp = pairfind(request->config_items, PW_CACHE_MERGE, 0, TAG_ANY);
145         if (vp && (vp->vp_integer == 0)) {
146                 RDEBUG2("Told not to merge entry into request");
147                 return;
148         }
149
150         RDEBUG2("Merging cache entry into request");
151
152         if (c->control) {
153                 RDEBUG2("Merging cached control list");
154                 radius_pairmove(request, &request->config_items, paircopy(request, c->control), false);
155         }
156
157         if (c->packet && request->packet) {
158                 RDEBUG2("Merging cached request list");
159                 radius_pairmove(request, &request->packet->vps, paircopy(request->packet, c->packet), false);
160         }
161
162         if (c->reply && request->reply) {
163                 RDEBUG2("Merging cached reply list");
164                 radius_pairmove(request, &request->reply->vps, paircopy(request->reply, c->reply), false);
165         }
166
167         if (inst->stats) {
168                 vp = pairfind(request->packet->vps, PW_CACHE_ENTRY_HITS, 0, TAG_ANY);
169                 if (!vp) {
170                         vp = paircreate(request->packet, PW_CACHE_ENTRY_HITS, 0);
171                         rad_assert(vp != NULL);
172                         pairadd(&request->packet->vps, vp);
173                 }
174                 vp->vp_integer = c->hits;
175         }
176 }
177
178
179 /*
180  *      Find a cached entry.
181  */
182 static rlm_cache_entry_t *cache_find(rlm_cache_t *inst, REQUEST *request,
183                                      char const *key)
184 {
185         int ttl;
186         rlm_cache_entry_t *c, my_c;
187         VALUE_PAIR *vp;
188
189         /*
190          *      Look at the expiry heap.
191          */
192         c = fr_heap_peek(inst->heap);
193         if (!c) {
194                 rad_assert(rbtree_num_elements(inst->cache) == 0);
195                 return NULL;
196         }
197
198         /*
199          *      If it's time to expire an old entry, do so now.
200          */
201         if (c->expires < request->timestamp) {
202                 fr_heap_extract(inst->heap, c);
203                 rbtree_deletebydata(inst->cache, c);
204         }
205
206         /*
207          *      Is there an entry for this key?
208          */
209         my_c.key = key;
210         c = rbtree_finddata(inst->cache, &my_c);
211         if (!c) return NULL;
212
213         /*
214          *      Yes, but it expired, OR the "forget all" epoch has
215          *      passed.  Delete it, and pretend it doesn't exist.
216          */
217         if ((c->expires < request->timestamp) ||
218             (c->created < inst->epoch)) {
219         delete:
220                 RDEBUG("Removing expired entry");
221
222                 fr_heap_extract(inst->heap, c);
223                 rbtree_deletebydata(inst->cache, c);
224
225                 return NULL;
226         }
227
228         RDEBUG("Found entry for \"%s\"", key);
229
230         /*
231          *      Update the expiry time based on the TTL.
232          *      A TTL of 0 means "delete from the cache".
233          *      A TTL < 0 means "delete from the cache and recreate the entry".
234          */
235         vp = pairfind(request->config_items, PW_CACHE_TTL, 0, TAG_ANY);
236         if (vp) {
237                 if (vp->vp_signed <= 0) goto delete;
238
239                 ttl = vp->vp_signed;
240                 c->expires = request->timestamp + ttl;
241                 RDEBUG("Adding %d to the TTL", ttl);
242         }
243         c->hits++;
244
245         return c;
246 }
247
248
249 /** Callback for map_to_request
250  *
251  * Simplifies merging VALUE_PAIRs into the current request.
252  */
253 static int _cache_add(VALUE_PAIR **out, REQUEST *request, UNUSED value_pair_map_t const *map, void *ctx)
254 {
255         VALUE_PAIR *vp;
256
257         vp = talloc_get_type_abort(ctx, VALUE_PAIR);
258         /* map_to_request will reparent */
259         *out = paircopy(request, vp);
260
261         if (!*out) return -1;
262         return 0;
263 }
264
265 /*
266  *      Add an entry to the cache.
267  */
268 static rlm_cache_entry_t *cache_add(rlm_cache_t *inst, REQUEST *request, char const *key)
269 {
270         int ttl;
271         VALUE_PAIR *vp, *to_cache;
272         vp_cursor_t src_list, cached_request, cached_reply, cached_control;
273
274         bool merge = true;
275
276         value_pair_map_t const *map;
277
278         rlm_cache_entry_t *c;
279
280         if (rbtree_num_elements(inst->cache) >= inst->max_entries) {
281                 RDEBUG("Cache is full: %d entries", inst->max_entries);
282                 return NULL;
283         }
284
285         /*
286          *      TTL of 0 means "don't cache this entry"
287          */
288         vp = pairfind(request->config_items, PW_CACHE_TTL, 0, TAG_ANY);
289         if (vp && (vp->vp_signed == 0)) return NULL;
290
291         c = talloc_zero(NULL, rlm_cache_entry_t);
292         c->key = talloc_typed_strdup(c, key);
293         c->created = c->expires = request->timestamp;
294
295         /*
296          *      Use per-entry TTL if > 0, or globally defined one.
297          */
298         ttl = vp && (vp->vp_signed > 0) ? vp->vp_integer : inst->ttl;
299         c->expires += ttl;
300
301         RDEBUG("Creating entry for \"%s\"", key);
302
303         /*
304          *      Check to see if we need to merge the entry into the request
305          */
306         vp = pairfind(request->config_items, PW_CACHE_MERGE, 0, TAG_ANY);
307         if (vp && (vp->vp_integer == 0)) {
308                 merge = false;
309                 RDEBUG2("Told not to merge new entry into request");
310         }
311
312         fr_cursor_init(&cached_request, &c->packet);
313         fr_cursor_init(&cached_reply, &c->reply);
314         fr_cursor_init(&cached_control, &c->control);
315
316         for (map = inst->maps; map != NULL; map = map->next) {
317                 bool do_merge = merge;
318
319                 rad_assert(map->lhs && map->rhs);
320
321                 if (map_to_vp(&to_cache, request, map, NULL) < 0) {
322                         RDEBUG("Skipping %s", map->rhs->name);
323                         continue;
324                 }
325
326                 /*
327                  *      Merge attributes into the current request if:
328                  *        - Map specifies an xlat'd string.
329                  *        - Map specifies a literal string.
330                  *        - Map specifies an exec.
331                  *        - Map src and dst lists differ.
332                  *        - Map src and dst attributes differ
333                  *
334                  *       Unless Cache-Merge = no
335                  */
336                 if (do_merge) switch (map->rhs->type) {
337                 case TMPL_TYPE_LITERAL:
338                 case TMPL_TYPE_XLAT:
339                 case TMPL_TYPE_EXEC:
340                         break;
341
342                 case TMPL_TYPE_LIST:
343                         if (map->rhs->tmpl_list == map->lhs->tmpl_list) do_merge = false;
344                         break;
345
346                 case TMPL_TYPE_ATTR:
347                         rad_assert(map->lhs->type == TMPL_TYPE_ATTR);
348                         if (map->rhs->tmpl_da == map->lhs->tmpl_da) do_merge = false;
349                         break;
350
351                 default:
352                         do_merge = false;
353                 }
354
355                 /*
356                  *      Reparent the VPs map_to_vp may return multiple.
357                  */
358                 for (vp = fr_cursor_init(&src_list, &to_cache);
359                      vp;
360                      vp = fr_cursor_next(&src_list)) {
361                         VERIFY_VP(vp);
362
363                         /*
364                          *      Prevent people from accidentally caching
365                          *      cache control attributes.
366                          */
367                         if (map->rhs->type == TMPL_TYPE_LIST) switch (vp->da->attr) {
368                         case PW_CACHE_TTL:
369                         case PW_CACHE_STATUS_ONLY:
370                         case PW_CACHE_READ_ONLY:
371                         case PW_CACHE_MERGE:
372                         case PW_CACHE_ENTRY_HITS:
373                                 RDEBUG2("Skipping %s", vp->da->name);
374                                 continue;
375
376                         default:
377                                 break;
378                         }
379
380                         if (debug_flag) map_debug_log(request, map, vp);
381                         (void) talloc_steal(c, vp);
382
383                         vp->op = map->op;
384
385                         switch (map->lhs->tmpl_list) {
386                         case PAIR_LIST_REQUEST:
387                                 fr_cursor_insert(&cached_request, vp);
388                                 break;
389
390                         case PAIR_LIST_REPLY:
391                                 fr_cursor_insert(&cached_reply, vp);
392                                 break;
393
394                         case PAIR_LIST_CONTROL:
395                                 fr_cursor_insert(&cached_control, vp);
396                                 break;
397
398                         default:
399                                 rad_assert(0);  /* should have been caught by validation */
400                         }
401
402                         if (do_merge && map_dst_valid(request, map)) {
403                                 /* There's no reason for this to fail (we checked the dst was valid) */
404                                 RDEBUG2("Adding to request:");
405                                 if (map_to_request(request, map, _cache_add, vp) < 0) rad_assert(0);
406                         }
407                 }
408         }
409
410         if (!rbtree_insert(inst->cache, c)) {
411                 REDEBUG("FAILED adding entry for key %s", key);
412                 cache_entry_free(c);
413                 return NULL;
414         }
415
416         if (!fr_heap_insert(inst->heap, c)) {
417                 REDEBUG("FAILED adding entry for key %s", key);
418                 rbtree_deletebydata(inst->cache, c);
419                 return NULL;
420         }
421
422         RDEBUG("Inserted entry, TTL %d seconds", ttl);
423
424         return c;
425 }
426
427 /** Verify that a map in the cache section makes sense
428  *
429  */
430 static int cache_verify(value_pair_map_t *map, UNUSED void *ctx)
431 {
432         if (modcall_fixup_update(map, ctx) < 0) return -1;
433
434         if ((map->lhs->type != TMPL_TYPE_ATTR) &&
435             (map->lhs->type != TMPL_TYPE_LIST)) {
436                 cf_log_err(map->ci, "Left operand must be an attribute ref or a list");
437                 return -1;
438         }
439
440         switch (map->rhs->type) {
441         case TMPL_TYPE_EXEC:
442                 cf_log_err(map->ci, "Exec values are not allowed");
443                 return -1;
444         /*
445          *      Only =, :=, += and -= operators are supported for
446          *      cache entries.
447          */
448         case TMPL_TYPE_LITERAL:
449         case TMPL_TYPE_XLAT:
450         case TMPL_TYPE_ATTR:
451                 switch (map->op) {
452                 case T_OP_SET:
453                 case T_OP_EQ:
454                 case T_OP_SUB:
455                 case T_OP_ADD:
456                         break;
457
458                 default:
459                         cf_log_err(map->ci, "Operator \"%s\" not allowed for %s values",
460                                    fr_int2str(fr_tokens, map->op, "<INVALID>"),
461                                    fr_int2str(tmpl_types, map->rhs->type, "<INVALID>"));
462                         return -1;
463                 }
464         default:
465                 break;
466         }
467
468         return 0;
469 }
470
471 /*
472  *      Allow single attribute values to be retrieved from the cache.
473  */
474 static ssize_t cache_xlat(void *instance, REQUEST *request,
475                           char const *fmt, char *out, size_t freespace)
476 {
477         rlm_cache_entry_t       *c;
478         rlm_cache_t             *inst = instance;
479         VALUE_PAIR              *vp, *vps;
480         pair_lists_t            list;
481         DICT_ATTR const         *target;
482         char const              *p = fmt;
483         size_t                  len;
484         int                     ret = 0;
485
486         list = radius_list_name(&p, PAIR_LIST_REQUEST);
487
488         target = dict_attrbyname(p);
489         if (!target) {
490                 REDEBUG("Unknown attribute \"%s\"", p);
491                 return -1;
492         }
493
494         PTHREAD_MUTEX_LOCK(&inst->cache_mutex);
495         c = cache_find(inst, request, fmt);
496
497         if (!c) {
498                 RDEBUG("No cache entry for key \"%s\"", fmt);
499                 *out = '\0';
500                 goto done;
501         }
502
503         switch (list) {
504         case PAIR_LIST_REQUEST:
505                 vps = c->packet;
506                 break;
507
508         case PAIR_LIST_REPLY:
509                 vps = c->reply;
510                 break;
511
512         case PAIR_LIST_CONTROL:
513                 vps = c->control;
514                 break;
515
516         case PAIR_LIST_UNKNOWN:
517                 PTHREAD_MUTEX_UNLOCK(&inst->cache_mutex);
518                 REDEBUG("Unknown list qualifier in \"%s\"", fmt);
519                 return -1;
520
521         default:
522                 PTHREAD_MUTEX_UNLOCK(&inst->cache_mutex);
523                 REDEBUG("Unsupported list \"%s\"",
524                         fr_int2str(pair_lists, list, "<UNKNOWN>"));
525                 return -1;
526         }
527
528         vp = pairfind(vps, target->attr, target->vendor, TAG_ANY);
529         if (!vp) {
530                 RDEBUG("No instance of this attribute has been cached");
531                 *out = '\0';
532                 goto done;
533         }
534
535         len = vp_prints_value(out, freespace, vp, 0);
536         if (is_truncated(len, freespace)) {
537                 PTHREAD_MUTEX_UNLOCK(&inst->cache_mutex);
538                 REDEBUG("Insufficient buffer space to write cached value");
539                 return -1;
540         }
541 done:
542         PTHREAD_MUTEX_UNLOCK(&inst->cache_mutex);
543
544         return ret;
545 }
546
547 /*
548  *      Only free memory we allocated.  The strings allocated via
549  *      cf_section_parse() do not need to be freed.
550  */
551 static int mod_detach(void *instance)
552 {
553         rlm_cache_t *inst = instance;
554
555         talloc_free(inst->maps);
556
557         fr_heap_delete(inst->heap);
558         rbtree_free(inst->cache);
559
560 #ifdef HAVE_PTHREAD_H
561         pthread_mutex_destroy(&inst->cache_mutex);
562 #endif
563         return 0;
564 }
565
566
567 /*
568  *      Instantiate the module.
569  */
570 static int mod_instantiate(CONF_SECTION *conf, void *instance)
571 {
572         rlm_cache_t *inst = instance;
573
574         inst->cs = conf;
575
576         inst->xlat_name = cf_section_name2(conf);
577         if (!inst->xlat_name) {
578                 inst->xlat_name = cf_section_name1(conf);
579         }
580
581         /*
582          *      Register the cache xlat function
583          */
584         xlat_register(inst->xlat_name, cache_xlat, NULL, inst);
585
586         rad_assert(inst->key && *inst->key);
587
588         if (inst->ttl == 0) {
589                 cf_log_err_cs(conf, "Must set 'ttl' to non-zero");
590                 return -1;
591         }
592
593         if (inst->epoch != 0) {
594                 cf_log_err_cs(conf, "Must not set 'epoch' in the configuration files");
595                 return -1;
596         }
597
598 #ifdef HAVE_PTHREAD_H
599         if (pthread_mutex_init(&inst->cache_mutex, NULL) < 0) {
600                 ERROR("Failed initializing mutex: %s",
601                        fr_syserror(errno));
602                 return -1;
603         }
604 #endif
605
606         /*
607          *      The cache.
608          */
609
610         inst->cache = rbtree_create(NULL, cache_entry_cmp, cache_entry_free, 0);
611         if (!inst->cache) {
612                 ERROR("Failed to create cache");
613                 return -1;
614         }
615         fr_link_talloc_ctx_free(inst, inst->cache);
616
617         /*
618          *      The heap of entries to expire.
619          */
620         inst->heap = fr_heap_create(cache_heap_cmp,
621                                     offsetof(rlm_cache_entry_t, offset));
622         if (!inst->heap) {
623                 ERROR("Failed to create heap for the cache");
624                 return -1;
625         }
626
627         /*
628          *      Make sure the users don't screw up too badly.
629          */
630         if (map_afrom_cs(&inst->maps, cf_section_sub_find(inst->cs, "update"),
631                          PAIR_LIST_REQUEST, PAIR_LIST_REQUEST, cache_verify, NULL, MAX_ATTRMAP) < 0) {
632                 return -1;
633         }
634
635         if (!inst->maps) {
636                 cf_log_err_cs(inst->cs, "Cache config must contain an update section, and "
637                               "that section must not be empty");
638
639                 return -1;
640         }
641
642         return 0;
643 }
644
645 /*
646  *      Do caching checks.  Since we can update ANY VP list, we do
647  *      exactly the same thing for all sections (autz / auth / etc.)
648  *
649  *      If you want to cache something different in different sections,
650  *      configure another cache module.
651  */
652 static rlm_rcode_t CC_HINT(nonnull) mod_cache_it(void *instance, REQUEST *request)
653 {
654         rlm_cache_entry_t *c;
655         rlm_cache_t *inst = instance;
656         vp_cursor_t cursor;
657         VALUE_PAIR *vp;
658         char buffer[1024];
659         rlm_rcode_t rcode;
660
661         if (radius_xlat(buffer, sizeof(buffer), request, inst->key, NULL, NULL) < 0) {
662                 return RLM_MODULE_FAIL;
663         }
664
665         PTHREAD_MUTEX_LOCK(&inst->cache_mutex);
666         c = cache_find(inst, request, buffer);
667
668         /*
669          *      If yes, only return whether we found a valid cache entry
670          */
671         vp = pairfind(request->config_items, PW_CACHE_STATUS_ONLY, 0, TAG_ANY);
672         if (vp && vp->vp_integer) {
673                 rcode = c ? RLM_MODULE_OK:
674                             RLM_MODULE_NOTFOUND;
675                 goto done;
676         }
677
678         if (c) {
679                 cache_merge(inst, request, c);
680
681                 rcode = RLM_MODULE_OK;
682                 goto done;
683         }
684
685         vp = pairfind(request->config_items, PW_CACHE_READ_ONLY, 0, TAG_ANY);
686         if (vp && vp->vp_integer) {
687                 rcode = RLM_MODULE_NOTFOUND;
688                 goto done;
689         }
690
691         c = cache_add(inst, request, buffer);
692         if (!c) {
693                 rcode = RLM_MODULE_NOOP;
694                 goto done;
695         }
696
697         rcode = RLM_MODULE_UPDATED;
698
699 done:
700         PTHREAD_MUTEX_UNLOCK(&inst->cache_mutex);
701
702         /*
703          *      Reset control attributes
704          */
705         for (vp = fr_cursor_init(&cursor, &request->config_items);
706              vp;
707              vp = fr_cursor_next(&cursor)) {
708                 if (vp->da->vendor == 0) switch (vp->da->attr) {
709                 case PW_CACHE_TTL:
710                 case PW_CACHE_READ_ONLY:
711                 case PW_CACHE_MERGE:
712                         vp = fr_cursor_remove(&cursor);
713                         talloc_free(vp);
714                         break;
715                 }
716         }
717
718         return rcode;
719 }
720
721
722 /*
723  *      The module name should be the only globally exported symbol.
724  *      That is, everything else should be 'static'.
725  *
726  *      If the module needs to temporarily modify it's instantiation
727  *      data, the type should be changed to RLM_TYPE_THREAD_UNSAFE.
728  *      The server will then take care of ensuring that the module
729  *      is single-threaded.
730  */
731 module_t rlm_cache = {
732         RLM_MODULE_INIT,
733         "cache",
734         0,                              /* type */
735         sizeof(rlm_cache_t),
736         module_config,
737         mod_instantiate,                /* instantiation */
738         mod_detach,                     /* detach */
739         {
740                 NULL,                   /* authentication */
741                 mod_cache_it,           /* authorization */
742                 mod_cache_it,           /* preaccounting */
743                 mod_cache_it,           /* accounting */
744                 NULL,                   /* checksimul */
745                 mod_cache_it,           /* pre-proxy */
746                 mod_cache_it,           /* post-proxy */
747                 mod_cache_it,           /* post-auth */
748         },
749 };