7d2c3b2967952f1ecbdb09b96973557d681d76a0
[m6w6/mod-domaintree] / mod_domaintree.c
1 /*
2 * mod_domaintree.c - Apache2
3 *
4 * $Id$
5 *
6 * Copyright 2005 Michael Wallner <mike@iworks.at>
7 *
8 * Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
11 *
12 * http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 *
20 *
21 * <pre>
22 * DomainTreeEnabled On
23 * DomainTreeMaxdepth 25
24 * DomainTreeStripWWW On
25 * DomainTreePrefix /sites
26 * DomainTreeSuffix /html
27 * DomainTreeCache 5
28 * DomainTreeAliasRecursion Off
29 * DomainTreeAlias /??/exmaple /com/exmaple
30 * DomainTreeAlias /???/example /com/example
31 * DomainTreeAlias /*one/ /anyone/
32 *
33 * /sites
34 * +- /at
35 * | +- /co
36 * | | +- /company
37 * | | +- /html
38 * | | +- /sub1
39 * | | | +- /html
40 * | | +- /sub2
41 * | | +- /html
42 * | +- /or
43 * | +- /organisation
44 * | +- /html
45 * +- /com
46 * +- /example
47 * +- /html
48 * </pre>
49 */
50
51 #ifdef HAVE_CONFIG_H
52 #include "config.h"
53 #endif
54
55 #define MODULE "mod_domaintree"
56 #define AUTHOR "<mike@iworks.at>"
57 #define VERSION "1.4"
58
59 /* {{{ Includes */
60
61 #include "apr.h"
62 #include "apr_lib.h"
63 #include "apr_hash.h"
64 #include "apr_tables.h"
65 #include "apr_strings.h"
66
67 #if APR_HAVE_UNISTD_H
68 #include <unistd.h>
69 #endif
70
71 #define APR_WANT_MEMFUNC
72 #define APR_WANT_STRFUNC
73 #include "apr_want.h"
74
75 #include "httpd.h"
76 #include "http_config.h"
77 #include "http_core.h"
78 #include "http_log.h"
79 #include "http_protocol.h"
80 #include "http_request.h"
81
82 /* }}} */
83 /* {{{ domaintree_module */
84
85 module AP_MODULE_DECLARE_DATA domaintree_module;
86
87 /* }}} */
88 /* {{{ Macros & Types */
89
90 #define DBG 0
91
92 #define MDT_CNF domaintree_conf
93 #define MDT_PTR (&domaintree_module)
94
95 #define GET_MDT_CNF(srv) ((MDT_CNF *) ap_get_module_config((srv)->module_config, MDT_PTR))
96
97 #define IF_SET_ELSE(a, b) (a != -1) ? (a) : (b)
98
99 #define NUL '\0'
100 #define EMPTY(str) ((str == NULL) || (*(str) == NUL))
101 #if DBG
102 # define local static
103 #else
104 # define local static APR_INLINE
105 #endif
106
107 #define DT_LOG_ERR ap_log_error(APLOG_MARK, APLOG_ERR, APR_SUCCESS, DT->server, "DomainTree: "
108 #define DT_LOG_WRN ap_log_error(APLOG_MARK, APLOG_WARNING, APR_SUCCESS, DT->server, "DomainTree: "
109 #define DT_LOG_DBG ap_log_error(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, DT->server, "DomainTree: "
110 #define DT_LOG_END );
111
112 typedef int STATUS;
113
114 typedef struct {
115 long recursion;
116 apr_table_t *faketable;
117 } aliases_t;
118
119 typedef struct {
120 char *host;
121 char *path;
122 apr_time_t lacc;
123 apr_pool_t *pool;
124 } dircache_entry_t;
125
126 typedef struct {
127 long clim;
128 apr_hash_t *hmap;
129 apr_pool_t *pool;
130 apr_global_mutex_t *lock;
131 } dircache_t;
132
133 typedef struct {
134 server_rec *server;
135 int enabled;
136 int stripwww;
137 int statroot;
138 long maxdepth;
139 char *prefix;
140 char *suffix;
141 aliases_t aliases;
142 dircache_t dircache;
143 } domaintree_conf;
144
145 /* }}} */
146 /* {{{ Helpers */
147
148 local char *strtr(char *string, char from, char to)
149 {
150 char *ptr = string;
151
152 if (from != to) {
153 while ((ptr = strchr(ptr, from))) {
154 *ptr = to;
155 }
156 }
157 return string;
158 }
159
160 #define TRIM_LEFT 1
161 #define TRIM_RIGHT 2
162 #define TRIM_BOTH (TRIM_LEFT|TRIM_RIGHT)
163 local char *trim(char *string, size_t length, char what, int where)
164 {
165 if (where & TRIM_RIGHT) {
166 while (length-- && (string[length] == what)) {
167 string[length] = NUL;
168 }
169 }
170 if (where & TRIM_LEFT) {
171 while (*string == what) {
172 ++string;
173 }
174 }
175 return string;
176 }
177
178 local int strmatch(const char *match, const char *string, const char **begin, const char **end)
179 {
180 *begin = *end = NULL;
181
182 while (*match)
183 {
184 switch (*match)
185 {
186 case '*':
187 while (*match == '*' || *match == '?') {
188 ++match;
189 }
190
191 if (!*begin) {
192 *begin = string;
193 }
194
195 if (!*match) {
196 *end = string + strlen(string);
197 return 1;
198 }
199
200 if (!(string = strchr(string, *match))) {
201 *end = string;
202 return 0;
203 }
204 break;
205
206 case '?':
207 if (!*begin) {
208 *begin = string;
209 }
210 ++string;
211 ++match;
212 break;
213
214 default:
215 if (*match == *string) {
216 if (!*begin) {
217 *begin = string;
218 }
219 ++match;
220 } else {
221 if (*begin) {
222 *end = string - 1;
223 return 0;
224 }
225 }
226 ++string;
227 break;
228 }
229 }
230
231 *end = string;
232 return 1;
233 }
234
235 local char *struniqchr(char *string, char uniq)
236 {
237 char *ptr = string;
238
239 while (ptr[0]) {
240 if (ptr[0] == uniq && ptr[1] == uniq) {
241 char *pos = &ptr[1];
242
243 for (; pos[1] == uniq; ++pos);
244 for (; pos[0]; ++pos) {
245 pos[0] = pos[1];
246 }
247 }
248 ++ptr;
249 }
250
251 return string;
252 }
253
254 local char *domaintree_host(apr_pool_t *pool, MDT_CNF *DT, const char *host_name)
255 {
256 size_t len;
257 char *port, *host;
258
259 if (EMPTY(host_name)) {
260 DT_LOG_WRN
261 "no host/server name"
262 DT_LOG_END
263 return NULL;
264 }
265
266 /* copy hostname */
267 host = apr_pstrdup(pool, host_name);
268
269 /* check for :NN port */
270 if ((port = strchr(host, ':'))) {
271 len = port - host;
272 *port = NUL;
273 } else {
274 len = strlen(host);
275 }
276
277 /* strip leading & trailing dots */
278 host = trim(host, len, '.', TRIM_BOTH);
279
280 DT_LOG_DBG
281 "host name = %s for %s", host, host_name
282 DT_LOG_END
283
284 return host;
285 }
286
287 local char *domaintree_path(apr_pool_t *pool, MDT_CNF *DT, const char *host_name)
288 {
289 long depth = 0, maxdepth = IF_SET_ELSE(DT->maxdepth, 20);
290 const char *host = host_name;
291 char *path = NULL, *host_ptr;
292
293 while ((host_ptr = strchr(host, '.'))) {
294
295 /* check max depth */
296 if (++depth > maxdepth) {
297 DT_LOG_ERR
298 "maxdepth exceeded (%ld)", maxdepth
299 DT_LOG_END
300 return NULL;
301 }
302
303 /* append part */
304 if (host_ptr - host) {
305
306 /* strip WWW */
307 if ((DT->stripwww > 0) && (depth == 1) && (!strncmp(host, "www.", sizeof("www")))) {
308 DT_LOG_DBG
309 "strip www"
310 DT_LOG_END
311 } else {
312 path = apr_pstrcat(pool, apr_pstrndup(pool, host, host_ptr - host), "/", path, NULL);
313 }
314 }
315
316 host = host_ptr + 1;
317 }
318
319 /* append last part if any and duplicate full path */
320 if (*host) {
321 path = apr_pstrcat(pool, host, "/", path, NULL);
322 }
323
324 DT_LOG_DBG
325 "path name = %s for %s", path, host_name
326 DT_LOG_END
327
328 return path;
329 }
330
331 local void domaintree_fake(apr_pool_t *pool, MDT_CNF *DT, char **path)
332 {
333 int i, more;
334 long recurlevel = 0;
335 apr_pool_t *local_pool;
336 const apr_array_header_t *header = apr_table_elts(DT->aliases.faketable);
337 apr_table_entry_t *array = (apr_table_entry_t *) header->elts;
338
339 if (APR_SUCCESS != apr_pool_create(&local_pool, pool)) {
340 return;
341 }
342
343 do {
344 more = 0;
345
346 if (recurlevel++ > DT->aliases.recursion) {
347 DT_LOG_ERR
348 "maximum alias recursion level (%ld) exceeded! "
349 "Check if you have recursive definitions of DomainTreeAlias directives.",
350 DT->aliases.recursion
351 DT_LOG_END
352 break;
353 }
354
355 for (i = 0; i < header->nelts; ++i) {
356 const char *begin, *end;
357
358 DT_LOG_DBG
359 "fake test = %s on %s", array[i].key, *path
360 DT_LOG_END
361
362 if (strmatch(array[i].key, *path, &begin, &end)) {
363 more = 1;
364 *path = apr_pstrcat(local_pool, "/", apr_pstrndup(local_pool, *path, begin - *path), "/", array[i].val, "/", end, NULL);
365
366 DT_LOG_DBG
367 "fake done = %s (%s <> %s)", *path, array[i].key, array[i].val
368 DT_LOG_END
369 }
370 }
371 } while (more && (DT->aliases.recursion > 0));
372
373 *path = apr_pstrdup(pool, struniqchr(*path, '/'));
374
375 apr_pool_destroy(local_pool);
376 }
377
378 local char *domaintree_cache_get(MDT_CNF *DT, apr_time_t atime, const char *host)
379 {
380 char *path = NULL;
381 dircache_entry_t *cache_entry;
382
383 apr_global_mutex_lock(DT->dircache.lock);
384
385 if ((cache_entry = apr_hash_get(DT->dircache.hmap, host, APR_HASH_KEY_STRING))) {
386 cache_entry->lacc = atime;
387 path = cache_entry->path;
388 }
389
390 apr_global_mutex_unlock(DT->dircache.lock);
391
392 if (path) {
393 DT_LOG_DBG
394 "cache hit = %s for %s", path, host
395 DT_LOG_END
396 }
397
398 return path;
399 }
400
401 local void domaintree_cache_set(MDT_CNF *DT, apr_time_t atime, const char *host, const char *path)
402 {
403 apr_pool_t *pool;
404 dircache_entry_t *cache_entry;
405
406 apr_pool_create(&pool, DT->dircache.pool);
407 cache_entry = apr_palloc(pool, sizeof(dircache_entry_t));
408
409 cache_entry->pool = pool;
410 cache_entry->lacc = atime;
411 cache_entry->host = apr_pstrdup(pool, host);
412 cache_entry->path = apr_pstrdup(pool, path);
413
414 apr_global_mutex_lock(DT->dircache.lock);
415
416 if (apr_hash_count(DT->dircache.hmap) >= DT->dircache.clim) {
417 apr_hash_index_t *idx;
418 dircache_entry_t *purge_this = NULL;
419
420 DT_LOG_WRN
421 "reached cache limit (%ld)", DT->dircache.clim
422 DT_LOG_END
423
424 for (idx = apr_hash_first(DT->dircache.pool, DT->dircache.hmap); idx; idx = apr_hash_next(idx)) {
425 dircache_entry_t *current;
426
427 apr_hash_this(idx, NULL, NULL, (void *) &current);
428 if ((!purge_this) || (purge_this->lacc > current->lacc)) {
429 purge_this = current;
430 }
431 }
432
433 if (purge_this) {
434 DT_LOG_DBG
435 "cache del = %s", purge_this->host
436 DT_LOG_END
437 apr_hash_set(DT->dircache.hmap, purge_this->host, APR_HASH_KEY_STRING, NULL);
438 apr_pool_destroy(purge_this->pool);
439 }
440 }
441 apr_hash_set(DT->dircache.hmap, cache_entry->host, APR_HASH_KEY_STRING, cache_entry);
442
443 apr_global_mutex_unlock(DT->dircache.lock);
444
445 DT_LOG_DBG
446 "cache set = %s for %s", path, host
447 DT_LOG_END
448 }
449
450 /* }}} */
451 /* {{{ Hooks */
452
453 static STATUS domaintree_hook_post_config(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *s)
454 {
455 ap_add_version_component(pconf, "MDT/" VERSION);
456 return OK;
457 }
458
459 static STATUS domaintree_hook_translate_name(request_rec *r)
460 {
461 MDT_CNF *DT = NULL;
462 char *host, *path, *docroot;
463
464 if ((!(DT = GET_MDT_CNF(r->server))) || (DT->enabled < 1)) {
465 return DECLINED;
466 }
467
468 #if DBG
469 DT_LOG_DBG
470 "processid = %d", (int) getpid()
471 DT_LOG_END
472 #endif
473
474 /* get a usable host name */
475 if (!(host = domaintree_host(r->pool, DT, ap_get_server_name(r)))) {
476 return DECLINED;
477 }
478
479 /* check cache */
480 if ((DT->dircache.clim < 1) || (!(path = domaintree_cache_get(DT, r->request_time, host)))) {
481 /* build path */
482 if (!(path = domaintree_path(r->pool, DT, host))) {
483 return DECLINED;
484 }
485
486 /* apply any aliases */
487 if (apr_table_elts(DT->aliases.faketable)->nelts) {
488 domaintree_fake(r->pool, DT, &path);
489 }
490
491 /* add to cache */
492 if (DT->dircache.clim > 0) {
493 domaintree_cache_set(DT, r->request_time, host, path);
494 }
495 }
496
497 /* compose virtual docroot */
498 docroot = struniqchr(apr_pstrcat(r->pool, DT->prefix, "/", path, "/", DT->suffix, "/", NULL), '/');
499
500 /* stat docroot */
501 if (DT->statroot > 0) {
502 apr_finfo_t sb;
503
504 switch (apr_stat(&sb, docroot, APR_FINFO_MIN, r->pool))
505 {
506 case APR_SUCCESS:
507 case APR_INCOMPLETE:
508 DT_LOG_DBG
509 "stat path = %s (success)", docroot
510 DT_LOG_END
511 break;
512
513 default:
514 DT_LOG_DBG
515 "stat path = %s (failure)", docroot
516 DT_LOG_END
517 return DECLINED;
518 break;
519 }
520 }
521
522 /* set virtual docroot */
523 apr_table_set(r->subprocess_env, "VIRTUAL_DOCUMENT_ROOT", docroot);
524
525 /* done */
526 r->canonical_filename = "";
527 r->filename = apr_pstrcat(r->pool, docroot, EMPTY(r->uri) ? NULL : ('/' == *r->uri ? r->uri + 1 : r->uri), NULL);
528
529 DT_LOG_DBG
530 "path done = %s", r->filename
531 DT_LOG_END
532
533 return OK;
534 }
535
536 static void domaintree_hooks(apr_pool_t *pool)
537 {
538 static const char * const pre[] = {"mod_alias.c", "mod_userdir.c", NULL};
539
540 ap_hook_post_config(domaintree_hook_post_config, NULL, NULL, APR_HOOK_MIDDLE);
541 ap_hook_translate_name(domaintree_hook_translate_name, pre, NULL, APR_HOOK_FIRST);
542 }
543
544 /* }}} */
545 /* {{{ Configuration */
546
547 static void *domaintree_create_srv(apr_pool_t *p, server_rec *s)
548 {
549 MDT_CNF *DT= (MDT_CNF *) apr_palloc(p, sizeof(MDT_CNF));
550
551 DT->server = s;
552 DT->enabled = -1;
553 DT->stripwww = -1;
554 DT->statroot = -1;
555 DT->maxdepth = -1;
556
557 DT->prefix = "/var/www";
558 DT->suffix = "public_html";
559
560 DT->aliases.recursion = -1;
561 DT->aliases.faketable = apr_table_make(p, 0);
562
563 DT->dircache.clim = -1;
564 DT->dircache.hmap = apr_hash_make(p);
565 apr_pool_create(&DT->dircache.pool, p);
566 apr_global_mutex_create(&DT->dircache.lock, __FILE__, APR_LOCK_DEFAULT, p);
567 #if DBG
568 fprintf(stderr, "MDT: cfg create %p\n", DT);
569 #endif
570 return DT;
571 }
572
573 static void *domaintree_merge_srv(apr_pool_t *p, void *old_cfg_ptr, void *new_cfg_ptr)
574 {
575 MDT_CNF *old_cfg = (MDT_CNF *) old_cfg_ptr, *new_cfg = (MDT_CNF *) new_cfg_ptr;
576 MDT_CNF *DT = (MDT_CNF *) apr_palloc(p, sizeof(MDT_CNF));
577
578 DT->server = new_cfg->server;
579 DT->enabled = IF_SET_ELSE(new_cfg->enabled, old_cfg->enabled);
580 DT->stripwww = IF_SET_ELSE(new_cfg->stripwww, old_cfg->stripwww);
581 DT->statroot = IF_SET_ELSE(new_cfg->statroot, old_cfg->statroot);
582 DT->maxdepth = IF_SET_ELSE(new_cfg->maxdepth, old_cfg->maxdepth);
583
584 DT->prefix = EMPTY(new_cfg->prefix) ? EMPTY(old_cfg->prefix) ? "/var/www" : old_cfg->prefix : new_cfg->prefix;
585 DT->suffix = EMPTY(new_cfg->suffix) ? EMPTY(old_cfg->suffix) ? "public_html" : old_cfg->suffix : new_cfg->suffix;
586
587 DT->aliases.recursion = IF_SET_ELSE(new_cfg->aliases.recursion, old_cfg->aliases.recursion);
588 DT->aliases.faketable = apr_table_overlay(p, new_cfg->aliases.faketable, old_cfg->aliases.faketable);
589
590 DT->dircache.clim = IF_SET_ELSE(new_cfg->dircache.clim, old_cfg->dircache.clim);
591 DT->dircache.hmap = apr_hash_overlay(p, new_cfg->dircache.hmap, old_cfg->dircache.hmap);
592 apr_global_mutex_create(&new_cfg->dircache.lock, __FILE__, APR_LOCK_DEFAULT, p);
593 #if DBG
594 fprintf(stderr, "MDT: cfg merge %p + %p = %p\n", old_cfg, new_cfg, DT);
595 #endif
596 return DT;
597 }
598
599 static const char *domaintree_enable(cmd_parms *cmd, void *conf, int flag)
600 {
601 GET_MDT_CNF(cmd->server)->enabled = flag;
602 return NULL;
603 }
604
605 static const char *domaintree_stripwww(cmd_parms *cmd, void *conf, int flag)
606 {
607 GET_MDT_CNF(cmd->server)->stripwww = flag;
608 return NULL;
609 }
610
611 static const char *domaintree_statroot(cmd_parms *cmd, void *conf, int flag)
612 {
613 GET_MDT_CNF(cmd->server)->statroot = flag;
614 return NULL;
615 }
616
617 static const char *domaintree_prefix(cmd_parms *cmd, void *conf, const char *prefix)
618 {
619 GET_MDT_CNF(cmd->server)->prefix = EMPTY(prefix) ? "/" : trim(apr_pstrdup(cmd->pool, prefix), strlen(prefix), '/', TRIM_RIGHT);
620 return NULL;
621 }
622
623 static const char *domaintree_suffix(cmd_parms *cmd, void *conf, const char *suffix)
624 {
625 GET_MDT_CNF(cmd->server)->suffix = EMPTY(suffix) ? "" : trim(apr_pstrdup(cmd->pool, suffix), strlen(suffix), '/', TRIM_BOTH);
626 return NULL;
627 }
628
629 static const char *domaintree_maxdepth(cmd_parms *cmd, void *conf, const char *max_depth)
630 {
631 long depth;
632
633 if ((depth = atol(max_depth))) {
634 if (depth >= 0L) {
635 GET_MDT_CNF(cmd->server)->maxdepth = depth;
636 } else {
637 return "Maximum DomainTree depth cannot be negative.";
638 }
639 }
640
641 return NULL;
642 }
643
644 static const char *domaintree_aliasrecursion(cmd_parms *cmd, void *conf, const char *alias_recursion)
645 {
646 long recursion;
647
648 if ((recursion = atol(alias_recursion))) {
649 if (recursion >= 0L) {
650 GET_MDT_CNF(cmd->server)->aliases.recursion = recursion;
651 } else {
652 return "DomainTree alias recursion cannot be negative.";
653 }
654 }
655
656 return NULL;
657 }
658
659 static const char *domaintree_alias(cmd_parms *cmd, void *conf, const char *fake, const char *real)
660 {
661 char *f = strtr(apr_pstrdup(cmd->pool, fake), '.', '/'), *r = strtr(apr_pstrdup(cmd->pool, real), '.', '/');
662
663 apr_table_set(GET_MDT_CNF(cmd->server)->aliases.faketable, trim(f, strlen(f), '/', TRIM_BOTH), trim(r, strlen(r), '/', TRIM_BOTH));
664
665 return NULL;
666 }
667
668 static const char *domaintree_cache(cmd_parms *cmd, void *conf, const char *cache)
669 {
670 long limit;
671
672 if ((limit = atol(cache))) {
673 if (limit >= 0L) {
674 GET_MDT_CNF(cmd->server)->dircache.clim = limit;
675 } else {
676 return "DomainTree cache limit cannot be negative.";
677 }
678 }
679
680 return NULL;
681 }
682
683 /* }}} */
684 /* {{{ Commands */
685
686 static command_rec domaintree_commands[] = {
687 AP_INIT_FLAG(
688 "DomainTreeEnabled", domaintree_enable, NULL, RSRC_CONF,
689 "Turn the module on or off."
690 ),
691
692 AP_INIT_FLAG(
693 "DomainTreeStripWWW", domaintree_stripwww, NULL, RSRC_CONF,
694 "Strip leading www from host. (default On)"
695 ),
696
697 AP_INIT_FLAG(
698 "DomainTreeStatRoot", domaintree_statroot, NULL, RSRC_CONF,
699 "Wheter to check for the evaluated virtual document root with a stat call. (default Off)"
700 ),
701
702 AP_INIT_TAKE1(
703 "DomainTreePrefix", domaintree_prefix, NULL, RSRC_CONF,
704 "DomainTree path prefix. (default /var/www) Do not forget the leading slash!"
705 ),
706
707 AP_INIT_TAKE1(
708 "DomainTreeSuffix", domaintree_suffix, NULL, RSRC_CONF,
709 "DomainTree path suffix. (default public_html)"
710 ),
711
712 AP_INIT_TAKE1(
713 "DomainTreeMaxdepth", domaintree_maxdepth, NULL, RSRC_CONF,
714 "DomainTree max path depth. (default 20)"
715 ),
716
717 AP_INIT_TAKE1(
718 "DomainTreeAliasRecursion", domaintree_aliasrecursion, NULL, RSRC_CONF,
719 "Whether (and how often at the maximum) DomainTree should walk recursively "
720 "through the aliases list as long as matching aliases are found. (default: 0 = turned off)"
721 ),
722
723 AP_INIT_TAKE2(
724 "DomainTreeAlias", domaintree_alias, NULL, RSRC_CONF,
725 "DomainTree aliases; e.g. DomainTreeAlias com/example/tickets com/example/support (dots or slashes equal)"
726 ),
727
728 AP_INIT_TAKE1(
729 "DomainTreeCache", domaintree_cache, NULL, RSRC_CONF,
730 "DomainTree server-wide host to directory cache; specify how many cache entries to allow (default: 0 = turned off)"
731 ),
732
733 { NULL }
734 };
735
736 /* }}} */
737 /* {{{ Module Administrativa */
738
739 module AP_MODULE_DECLARE_DATA domaintree_module = {
740 STANDARD20_MODULE_STUFF,
741 NULL, /* create per-dir */
742 NULL, /* merge per-dir */
743 domaintree_create_srv, /* create per-server */
744 domaintree_merge_srv, /* merge per-server */
745 domaintree_commands, /* config commands */
746 domaintree_hooks /* hooks */
747 };
748
749 /* }}} */
750
751 /*
752 * Local variables:
753 * tab-width: 4
754 * c-basic-offset: 4
755 * End:
756 * vim600: noet sw=4 ts=4 fdm=marker
757 * vim<600: noet sw=4 ts=4
758 */