1 /* fstatat() fdopendir() */
2 #if !defined(_XOPEN_SOURCE) || _XOPEN_SOURCE-0 < 700
3 #undef _XOPEN_SOURCE
4 #define _XOPEN_SOURCE 700
5 /* NetBSD dirent.h improperly hides fdopendir() (POSIX.1-2008) declaration
6 * which should be visible with _XOPEN_SOURCE 700 or _POSIX_C_SOURCE 200809L */
7 #ifdef __NetBSD__
8 #define _NETBSD_SOURCE
9 #endif
10 #endif
11
12 #include "first.h"
13
14 #include "sys-time.h"
15
16 #include "base.h"
17 #include "log.h"
18 #include "buffer.h"
19 #include "fdevent.h"
20 #include "http_chunk.h"
21 #include "http_etag.h"
22 #include "http_header.h"
23 #include "keyvalue.h"
24 #include "response.h"
25
26 #include "plugin.h"
27
28 #include "stat_cache.h"
29
30 #include <errno.h>
31 #include <stdlib.h>
32 #include <string.h>
33 #include <dirent.h>
34 #include <fcntl.h>
35 #include <unistd.h>
36
37 #ifdef AT_FDCWD
38 #ifndef _ATFILE_SOURCE
39 #define _ATFILE_SOURCE
40 #endif
41 #endif
42
43 #ifndef _D_EXACT_NAMLEN
44 #ifdef _DIRENT_HAVE_D_NAMLEN
45 #define _D_EXACT_NAMLEN(d) ((d)->d_namlen)
46 #else
47 #define _D_EXACT_NAMLEN(d) (strlen ((d)->d_name))
48 #endif
49 #endif
50
51 /**
52 * this is a dirlisting for a lighttpd plugin
53 *
54 * Notes:
55 * - mod_dirlisting is a dir list implementation. One size does not fit all.
56 * mod_dirlisting aims to be somewhat flexible, but if special behavior
57 * is needed, use custom CGI/FastCGI/SCGI to handle dir listing instead.
58 * - backend daemon could implement custom caching
59 * - backend daemon could monitor directory for changes (e.g. inotify)
60 * - backend daemon or scripts could trigger when directory is modified
61 * and could regenerate index.html (and mod_indexfile could be used
62 * instead of mod_dirlisting)
63 * - basic alphabetical sorting (in C locale) is done on server side
64 * in case client does not execute javascript
65 * (otherwise, response could be streamed, which is not done)
66 * (possible future dir-listing.* config option)
67 * - reading entire directory into memory for sorting large directory
68 * can lead to large memory usage if many simultaneous requests occur
69 */
70
71 struct dirlist_cache {
72 int32_t max_age;
73 buffer *path;
74 };
75
76 typedef struct {
77 char dir_listing;
78 char json;
79 char hide_dot_files;
80 char hide_readme_file;
81 char encode_readme;
82 char hide_header_file;
83 char encode_header;
84 char auto_layout;
85
86 pcre_keyvalue_buffer *excludes;
87
88 const buffer *show_readme;
89 const buffer *show_header;
90 const buffer *external_css;
91 const buffer *external_js;
92 const buffer *encoding;
93 const buffer *set_footer;
94 const struct dirlist_cache *cache;
95 } plugin_config;
96
97 typedef struct {
98 PLUGIN_DATA;
99 plugin_config defaults;
100 plugin_config conf;
101 int processing;
102 } plugin_data;
103
104 typedef struct {
105 uint32_t namelen;
106 unix_time64_t mtime;
107 off_t size;
108 } dirls_entry_t;
109
110 typedef struct {
111 dirls_entry_t **ent;
112 uint32_t used;
113 } dirls_list_t;
114
115 #define DIRLIST_ENT_NAME(ent) ((char*)(ent) + sizeof(dirls_entry_t))
116 /* DIRLIST_BLOB_SIZE must be power of 2 for current internal usage */
117 #define DIRLIST_BLOB_SIZE 16
118
119 typedef struct {
120 DIR *dp;
121 dirls_list_t dirs;
122 dirls_list_t files;
123 char *path;
124 char *path_file;
125 int dfd; /*(dirfd() owned by (DIR *))*/
126 uint32_t name_max;
127 buffer *jb;
128 int jcomma;
129 int jfd;
130 char *jfn;
131 uint32_t jfn_len;
132 plugin_config conf;
133 } handler_ctx;
134
135 #define DIRLIST_BATCH 32
136 static int dirlist_max_in_progress;
137
138
mod_dirlisting_handler_ctx_init(plugin_data * const p)139 static handler_ctx * mod_dirlisting_handler_ctx_init (plugin_data * const p) {
140 handler_ctx *hctx = ck_calloc(1, sizeof(*hctx));
141 memcpy(&hctx->conf, &p->conf, sizeof(plugin_config));
142 return hctx;
143 }
144
mod_dirlisting_handler_ctx_free(handler_ctx * hctx)145 static void mod_dirlisting_handler_ctx_free (handler_ctx *hctx) {
146 if (hctx->dp)
147 closedir(hctx->dp);
148 if (hctx->files.ent) {
149 dirls_entry_t ** const ent = hctx->files.ent;
150 for (uint32_t i = 0, used = hctx->files.used; i < used; ++i)
151 free(ent[i]);
152 free(ent);
153 }
154 if (hctx->dirs.ent) {
155 dirls_entry_t ** const ent = hctx->dirs.ent;
156 for (uint32_t i = 0, used = hctx->dirs.used; i < used; ++i)
157 free(ent[i]);
158 free(ent);
159 }
160 if (hctx->jb) {
161 chunk_buffer_release(hctx->jb);
162 if (hctx->jfn) {
163 unlink(hctx->jfn);
164 free(hctx->jfn);
165 }
166 if (-1 != hctx->jfd)
167 close(hctx->jfd);
168 }
169 free(hctx->path);
170 free(hctx);
171 }
172
173
mod_dirlisting_parse_cache(server * srv,const array * a)174 static struct dirlist_cache * mod_dirlisting_parse_cache(server *srv, const array *a) {
175 const data_unset *du;
176
177 du = array_get_element_klen(a, CONST_STR_LEN("max-age"));
178 const int32_t max_age = config_plugin_value_to_int32(du, 15);
179
180 buffer *path = NULL;
181 du = array_get_element_klen(a, CONST_STR_LEN("path"));
182 if (NULL == du) {
183 if (0 != max_age) {
184 log_error(srv->errh, __FILE__, __LINE__,
185 "dir-listing.cache must include \"path\"");
186 return NULL;
187 }
188 }
189 else {
190 if (du->type != TYPE_STRING) {
191 log_error(srv->errh, __FILE__, __LINE__,
192 "dir-listing.cache \"path\" must have string value");
193 return NULL;
194 }
195 path = &((data_string *)du)->value;
196 if (!stat_cache_path_isdir(path)) {
197 if (errno == ENOTDIR) {
198 log_error(srv->errh, __FILE__, __LINE__,
199 "dir-listing.cache \"path\" => \"%s\" is not a dir",
200 path->ptr);
201 return NULL;
202 }
203 if (errno == ENOENT) {
204 log_error(srv->errh, __FILE__, __LINE__,
205 "dir-listing.cache \"path\" => \"%s\" does not exist",
206 path->ptr);
207 /*(warning; not returning NULL)*/
208 }
209 }
210 }
211
212 struct dirlist_cache * const cache =
213 ck_calloc(1, sizeof(struct dirlist_cache));
214 cache->max_age = max_age;
215 cache->path = path;
216 return cache;
217 }
218
219
mod_dirlisting_parse_excludes(server * srv,const array * a)220 static pcre_keyvalue_buffer * mod_dirlisting_parse_excludes(server *srv, const array *a) {
221 const int pcre_jit = config_feature_bool(srv, "server.pcre_jit", 1);
222 pcre_keyvalue_buffer * const kvb = pcre_keyvalue_buffer_init();
223 buffer empty = { NULL, 0, 0 };
224 for (uint32_t j = 0; j < a->used; ++j) {
225 const data_string *ds = (data_string *)a->data[j];
226 if (!pcre_keyvalue_buffer_append(srv->errh, kvb, &ds->value, &empty,
227 pcre_jit)) {
228 log_error(srv->errh, __FILE__, __LINE__,
229 "pcre_compile failed for %s", ds->key.ptr);
230 pcre_keyvalue_buffer_free(kvb);
231 return NULL;
232 }
233 }
234 return kvb;
235 }
236
237 #ifdef __COVERITY__
238 #include "burl.h"
239 #endif
240
mod_dirlisting_exclude(pcre_keyvalue_buffer * const kvb,const char * const name,const uint32_t len)241 static int mod_dirlisting_exclude(pcre_keyvalue_buffer * const kvb, const char * const name, const uint32_t len) {
242 /*(re-use keyvalue.[ch] for match-only;
243 * must have been configured with empty kvb 'value' during init)*/
244 buffer input = { NULL, len+1, 0 };
245 *(const char **)&input.ptr = name;
246 pcre_keyvalue_ctx ctx = { NULL, NULL, -1, 0, NULL, NULL };
247 #ifdef __COVERITY__
248 /*(again, must have been configured w/ empty kvb 'value' during init)*/
249 struct cond_match_t cache;
250 memset(&cache, 0, sizeof(cache));
251 struct burl_parts_t bp;
252 memset(&bp, 0, sizeof(bp));
253 ctx.cache = &cache;
254 ctx.burl = &bp;
255 #endif
256 /*(fail closed (simulate match to exclude) if there is an error)*/
257 return HANDLER_ERROR == pcre_keyvalue_buffer_process(kvb,&ctx,&input,NULL)
258 || -1 != ctx.m;
259 }
260
261
INIT_FUNC(mod_dirlisting_init)262 INIT_FUNC(mod_dirlisting_init) {
263 return ck_calloc(1, sizeof(plugin_data));
264 }
265
FREE_FUNC(mod_dirlisting_free)266 FREE_FUNC(mod_dirlisting_free) {
267 plugin_data * const p = p_d;
268 if (NULL == p->cvlist) return;
269 /* (init i to 0 if global context; to 1 to skip empty global context) */
270 for (int i = !p->cvlist[0].v.u2[1], used = p->nconfig; i < used; ++i) {
271 config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
272 for (; -1 != cpv->k_id; ++cpv) {
273 switch (cpv->k_id) {
274 case 2: /* dir-listing.exclude */
275 if (cpv->vtype != T_CONFIG_LOCAL) continue;
276 pcre_keyvalue_buffer_free(cpv->v.v);
277 break;
278 case 15: /* dir-listing.cache */
279 if (cpv->vtype != T_CONFIG_LOCAL) continue;
280 free(cpv->v.v);
281 break;
282 default:
283 break;
284 }
285 }
286 }
287 }
288
mod_dirlisting_merge_config_cpv(plugin_config * const pconf,const config_plugin_value_t * const cpv)289 static void mod_dirlisting_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) {
290 switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */
291 case 0: /* dir-listing.activate */
292 case 1: /* server.dir-listing *//*(historical)*/
293 pconf->dir_listing = (char)cpv->v.u;
294 break;
295 case 2: /* dir-listing.exclude */
296 if (cpv->vtype == T_CONFIG_LOCAL)
297 pconf->excludes = cpv->v.v;
298 break;
299 case 3: /* dir-listing.hide-dotfiles */
300 pconf->hide_dot_files = (char)cpv->v.u;
301 break;
302 case 4: /* dir-listing.external-css */
303 pconf->external_css = cpv->v.b;
304 break;
305 case 5: /* dir-listing.external-js */
306 pconf->external_js = cpv->v.b;
307 break;
308 case 6: /* dir-listing.encoding */
309 pconf->encoding = cpv->v.b;
310 break;
311 case 7: /* dir-listing.show-readme */
312 pconf->show_readme = cpv->v.b;
313 break;
314 case 8: /* dir-listing.hide-readme-file */
315 pconf->hide_readme_file = (char)cpv->v.u;
316 break;
317 case 9: /* dir-listing.show-header */
318 pconf->show_header = cpv->v.b;
319 break;
320 case 10:/* dir-listing.hide-header-file */
321 pconf->hide_header_file = (char)cpv->v.u;
322 break;
323 case 11:/* dir-listing.set-footer */
324 pconf->set_footer = cpv->v.b;
325 break;
326 case 12:/* dir-listing.encode-readme */
327 pconf->encode_readme = (char)cpv->v.u;
328 break;
329 case 13:/* dir-listing.encode-header */
330 pconf->encode_header = (char)cpv->v.u;
331 break;
332 case 14:/* dir-listing.auto-layout */
333 pconf->auto_layout = (char)cpv->v.u;
334 break;
335 case 15:/* dir-listing.cache */
336 if (cpv->vtype == T_CONFIG_LOCAL)
337 pconf->cache = cpv->v.v;
338 break;
339 default:/* should not happen */
340 return;
341 }
342 }
343
mod_dirlisting_merge_config(plugin_config * const pconf,const config_plugin_value_t * cpv)344 static void mod_dirlisting_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) {
345 do {
346 mod_dirlisting_merge_config_cpv(pconf, cpv);
347 } while ((++cpv)->k_id != -1);
348 }
349
mod_dirlisting_patch_config(request_st * const r,plugin_data * const p)350 static void mod_dirlisting_patch_config(request_st * const r, plugin_data * const p) {
351 memcpy(&p->conf, &p->defaults, sizeof(plugin_config));
352 for (int i = 1, used = p->nconfig; i < used; ++i) {
353 if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id))
354 mod_dirlisting_merge_config(&p->conf, p->cvlist + p->cvlist[i].v.u2[0]);
355 }
356 }
357
SETDEFAULTS_FUNC(mod_dirlisting_set_defaults)358 SETDEFAULTS_FUNC(mod_dirlisting_set_defaults) {
359 static const config_plugin_keys_t cpk[] = {
360 { CONST_STR_LEN("dir-listing.activate"),
361 T_CONFIG_BOOL,
362 T_CONFIG_SCOPE_CONNECTION }
363 ,{ CONST_STR_LEN("server.dir-listing"),
364 T_CONFIG_BOOL,
365 T_CONFIG_SCOPE_CONNECTION }
366 ,{ CONST_STR_LEN("dir-listing.exclude"),
367 T_CONFIG_ARRAY_VLIST,
368 T_CONFIG_SCOPE_CONNECTION }
369 ,{ CONST_STR_LEN("dir-listing.hide-dotfiles"),
370 T_CONFIG_BOOL,
371 T_CONFIG_SCOPE_CONNECTION }
372 ,{ CONST_STR_LEN("dir-listing.external-css"),
373 T_CONFIG_STRING,
374 T_CONFIG_SCOPE_CONNECTION }
375 ,{ CONST_STR_LEN("dir-listing.external-js"),
376 T_CONFIG_STRING,
377 T_CONFIG_SCOPE_CONNECTION }
378 ,{ CONST_STR_LEN("dir-listing.encoding"),
379 T_CONFIG_STRING,
380 T_CONFIG_SCOPE_CONNECTION }
381 ,{ CONST_STR_LEN("dir-listing.show-readme"),
382 T_CONFIG_STRING,
383 T_CONFIG_SCOPE_CONNECTION }
384 ,{ CONST_STR_LEN("dir-listing.hide-readme-file"),
385 T_CONFIG_BOOL,
386 T_CONFIG_SCOPE_CONNECTION }
387 ,{ CONST_STR_LEN("dir-listing.show-header"),
388 T_CONFIG_STRING,
389 T_CONFIG_SCOPE_CONNECTION }
390 ,{ CONST_STR_LEN("dir-listing.hide-header-file"),
391 T_CONFIG_BOOL,
392 T_CONFIG_SCOPE_CONNECTION }
393 ,{ CONST_STR_LEN("dir-listing.set-footer"),
394 T_CONFIG_STRING,
395 T_CONFIG_SCOPE_CONNECTION }
396 ,{ CONST_STR_LEN("dir-listing.encode-readme"),
397 T_CONFIG_BOOL,
398 T_CONFIG_SCOPE_CONNECTION }
399 ,{ CONST_STR_LEN("dir-listing.encode-header"),
400 T_CONFIG_BOOL,
401 T_CONFIG_SCOPE_CONNECTION }
402 ,{ CONST_STR_LEN("dir-listing.auto-layout"),
403 T_CONFIG_BOOL,
404 T_CONFIG_SCOPE_CONNECTION }
405 ,{ CONST_STR_LEN("dir-listing.cache"),
406 T_CONFIG_ARRAY_KVANY,
407 T_CONFIG_SCOPE_CONNECTION }
408 ,{ NULL, 0,
409 T_CONFIG_UNSET,
410 T_CONFIG_SCOPE_UNSET }
411 };
412
413 plugin_data * const p = p_d;
414 if (!config_plugin_values_init(srv, p, cpk, "mod_dirlisting"))
415 return HANDLER_ERROR;
416
417 /* process and validate config directives
418 * (init i to 0 if global context; to 1 to skip empty global context) */
419 for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) {
420 config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
421 for (; -1 != cpv->k_id; ++cpv) {
422 switch (cpv->k_id) {
423 case 0: /* dir-listing.activate */
424 case 1: /* server.dir-listing *//*(historical)*/
425 break;
426 case 2: /* dir-listing.exclude */
427 cpv->v.v = mod_dirlisting_parse_excludes(srv, cpv->v.a);
428 if (NULL == cpv->v.v) return HANDLER_ERROR;
429 cpv->vtype = T_CONFIG_LOCAL;
430 break;
431 case 3: /* dir-listing.hide-dotfiles */
432 break;
433 case 4: /* dir-listing.external-css */
434 case 5: /* dir-listing.external-js */
435 case 6: /* dir-listing.encoding */
436 if (buffer_is_blank(cpv->v.b))
437 cpv->v.b = NULL;
438 break;
439 case 7: /* dir-listing.show-readme */
440 if (!buffer_is_blank(cpv->v.b)) {
441 buffer *b;
442 *(const buffer **)&b = cpv->v.b;
443 if (buffer_is_equal_string(b, CONST_STR_LEN("enable")))
444 buffer_copy_string_len(b, CONST_STR_LEN("README.txt"));
445 else if (buffer_is_equal_string(b,CONST_STR_LEN("disable")))
446 buffer_clear(b);
447 }
448 else
449 cpv->v.b = NULL;
450 break;
451 case 8: /* dir-listing.hide-readme-file */
452 break;
453 case 9: /* dir-listing.show-header */
454 if (!buffer_is_blank(cpv->v.b)) {
455 buffer *b;
456 *(const buffer **)&b = cpv->v.b;
457 if (buffer_is_equal_string(b, CONST_STR_LEN("enable")))
458 buffer_copy_string_len(b, CONST_STR_LEN("HEADER.txt"));
459 else if (buffer_is_equal_string(b,CONST_STR_LEN("disable")))
460 buffer_clear(b);
461 }
462 else
463 cpv->v.b = NULL;
464 break;
465 case 10:/* dir-listing.hide-header-file */
466 break;
467 case 11:/* dir-listing.set-footer */
468 if (buffer_is_blank(cpv->v.b))
469 cpv->v.b = NULL;
470 break;
471 case 12:/* dir-listing.encode-readme */
472 case 13:/* dir-listing.encode-header */
473 case 14:/* dir-listing.auto-layout */
474 break;
475 case 15:/* dir-listing.cache */
476 cpv->v.v = mod_dirlisting_parse_cache(srv, cpv->v.a);
477 if (NULL == cpv->v.v) return HANDLER_ERROR;
478 if (0 == ((struct dirlist_cache *)cpv->v.v)->max_age) {
479 free(cpv->v.v);
480 cpv->v.v = NULL; /*(to disable after having been enabled)*/
481 }
482 cpv->vtype = T_CONFIG_LOCAL;
483 break;
484 default:/* should not happen */
485 break;
486 }
487 }
488 }
489
490 dirlist_max_in_progress = srv->srvconf.max_conns >> 4;
491 if (0 == dirlist_max_in_progress) dirlist_max_in_progress = 1;
492
493 p->defaults.dir_listing = 0;
494 p->defaults.json = 0;
495 p->defaults.hide_dot_files = 1;
496 p->defaults.hide_readme_file = 0;
497 p->defaults.hide_header_file = 0;
498 p->defaults.encode_readme = 1;
499 p->defaults.encode_header = 1;
500 p->defaults.auto_layout = 1;
501
502 /* initialize p->defaults from global config context */
503 if (p->nconfig > 0 && p->cvlist->v.u2[1]) {
504 const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0];
505 if (-1 != cpv->k_id)
506 mod_dirlisting_merge_config(&p->defaults, cpv);
507 }
508
509 return HANDLER_GO_ON;
510 }
511
512 /* simple combsort algorithm */
http_dirls_sort(dirls_entry_t ** ent,int num)513 static void http_dirls_sort(dirls_entry_t **ent, int num) {
514 int gap = num;
515 int i, j;
516 int swapped;
517 dirls_entry_t *tmp;
518
519 do {
520 gap = (gap * 10) / 13;
521 if (gap == 9 || gap == 10)
522 gap = 11;
523 if (gap < 1)
524 gap = 1;
525 swapped = 0;
526
527 for (i = 0; i < num - gap; i++) {
528 j = i + gap;
529 if (strcmp(DIRLIST_ENT_NAME(ent[i]), DIRLIST_ENT_NAME(ent[j])) > 0) {
530 tmp = ent[i];
531 ent[i] = ent[j];
532 ent[j] = tmp;
533 swapped = 1;
534 }
535 }
536
537 } while (gap > 1 || swapped);
538 }
539
540 /* buffer must be able to hold "999.9K"
541 * conversion is simple but not perfect
542 */
http_list_directory_sizefmt(char * buf,size_t bufsz,off_t size)543 static size_t http_list_directory_sizefmt(char *buf, size_t bufsz, off_t size) {
544 int remain;
545 int u = -1; /* u will always increment at least once */
546 size_t buflen;
547
548 if (0 < size && size < 100)
549 size += 99;
550
551 do {
552 remain = (int)(size & 1023);
553 size >>= 10;
554 ++u;
555 } while (size & ~1023);
556
557 remain /= 100;
558 if (remain > 9)
559 remain = 9;
560 if (size > 999) {
561 size = 0;
562 remain = 9;
563 u++;
564 }
565
566 buflen = li_itostrn(buf, bufsz, size);
567 if (buflen + 3 >= bufsz) return buflen;
568 buf[buflen+0] = '.';
569 buf[buflen+1] = remain + '0';
570 buf[buflen+2] = "KMGTPE"[u]; /* Kilo, Mega, Giga, Tera, Peta, Exa */
571 buf[buflen+3] = '\0';
572
573 return buflen + 3;
574 }
575
http_list_directory_include_file(request_st * const r,const handler_ctx * const p,int is_header)576 static void http_list_directory_include_file(request_st * const r, const handler_ctx * const p, int is_header) {
577 const buffer *path;
578 int encode = 0;
579 if (is_header) {
580 path = p->conf.show_header;
581 encode = p->conf.encode_header;
582 }
583 else {
584 path = p->conf.show_readme;
585 encode = p->conf.encode_readme;
586 }
587 if (NULL == path) return;
588
589 uint32_t len = 0;
590 if (path->ptr[0] != '/') { /* temporarily extend r->physical.path */
591 len = buffer_clen(&r->physical.path);
592 buffer_append_path_len(&r->physical.path, BUF_PTR_LEN(path));
593 path = &r->physical.path;
594 }
595 stat_cache_entry * const sce =
596 stat_cache_get_entry_open(path, r->conf.follow_symlink);
597 if (len)
598 buffer_truncate(&r->physical.path, len);
599 if (NULL == sce || sce->fd < 0 || 0 == sce->st.st_size)
600 return;
601
602 chunkqueue * const cq = &r->write_queue;
603 if (encode) {
604 if (is_header)
605 chunkqueue_append_mem(cq, CONST_STR_LEN("<pre class=\"header\">"));
606 else
607 chunkqueue_append_mem(cq, CONST_STR_LEN("<pre class=\"readme\">"));
608
609 /* Note: encoding a very large file may cause lighttpd to pause handling
610 * other requests while lighttpd encodes the file, especially if file is
611 * on a remote filesystem */
612
613 /* encoding can consume 6x file size in worst case scenario,
614 * so send encoded contents of files > 32k to tempfiles) */
615 buffer * const tb = r->tmp_buf;
616 buffer * const out = sce->st.st_size <= 32768
617 ? chunkqueue_append_buffer_open(cq)
618 : tb;
619 buffer_clear(out);
620 const int fd = sce->fd;
621 ssize_t rd;
622 char buf[8192];
623 while ((rd = read(fd, buf, sizeof(buf))) > 0) {
624 buffer_append_string_encoded(out, buf, (size_t)rd, ENCODING_MINIMAL_XML);
625 if (out == tb) {
626 if (0 != chunkqueue_append_mem_to_tempfile(cq,
627 BUF_PTR_LEN(out),
628 r->conf.errh))
629 break;
630 buffer_clear(out);
631 }
632 }
633 if (out != tb)
634 chunkqueue_append_buffer_commit(cq);
635
636 chunkqueue_append_mem(cq, CONST_STR_LEN("</pre>"));
637 }
638 else {
639 (void)http_chunk_append_file_ref(r, sce);
640 }
641 }
642
643 /* portions copied from mod_status
644 * modified and specialized for stable dirlist sorting by name */
645 static const char js_simple_table_resort[] = \
646 "var click_column;\n" \
647 "var name_column = 0;\n" \
648 "var date_column = 1;\n" \
649 "var size_column = 2;\n" \
650 "var type_column = 3;\n" \
651 "var prev_span = null;\n" \
652 "\n" \
653 "if (typeof(String.prototype.localeCompare) === 'undefined') {\n" \
654 " String.prototype.localeCompare = function(str, locale, options) {\n" \
655 " return ((this == str) ? 0 : ((this > str) ? 1 : -1));\n" \
656 " };\n" \
657 "}\n" \
658 "\n" \
659 "if (typeof(String.prototype.toLocaleUpperCase) === 'undefined') {\n" \
660 " String.prototype.toLocaleUpperCase = function() {\n" \
661 " return this.toUpperCase();\n" \
662 " };\n" \
663 "}\n" \
664 "\n" \
665 "function get_inner_text(el) {\n" \
666 " if((typeof el == 'string')||(typeof el == 'undefined'))\n" \
667 " return el;\n" \
668 " if(el.innerText)\n" \
669 " return el.innerText;\n" \
670 " else {\n" \
671 " var str = \"\";\n" \
672 " var cs = el.childNodes;\n" \
673 " var l = cs.length;\n" \
674 " for (var i=0;i<l;i++) {\n" \
675 " if (cs[i].nodeType==1) str += get_inner_text(cs[i]);\n" \
676 " else if (cs[i].nodeType==3) str += cs[i].nodeValue;\n" \
677 " }\n" \
678 " }\n" \
679 " return str;\n" \
680 "}\n" \
681 "\n" \
682 "function isdigit(c) {\n" \
683 " return (c >= '0' && c <= '9');\n" \
684 "}\n" \
685 "\n" \
686 "function unit_multiplier(unit) {\n" \
687 " return (unit=='K') ? 1000\n" \
688 " : (unit=='M') ? 1000000\n" \
689 " : (unit=='G') ? 1000000000\n" \
690 " : (unit=='T') ? 1000000000000\n" \
691 " : (unit=='P') ? 1000000000000000\n" \
692 " : (unit=='E') ? 1000000000000000000 : 1;\n" \
693 "}\n" \
694 "\n" \
695 "var li_date_regex=/(\\d{4})-(\\w{3})-(\\d{2}) (\\d{2}):(\\d{2}):(\\d{2})/;\n" \
696 "\n" \
697 "var li_mon = ['Jan','Feb','Mar','Apr','May','Jun',\n" \
698 " 'Jul','Aug','Sep','Oct','Nov','Dec'];\n" \
699 "\n" \
700 "function li_mon_num(mon) {\n" \
701 " var i; for (i = 0; i < 12 && mon != li_mon[i]; ++i); return i;\n" \
702 "}\n" \
703 "\n" \
704 "function li_date_cmp(s1, s2) {\n" \
705 " var dp1 = li_date_regex.exec(s1)\n" \
706 " var dp2 = li_date_regex.exec(s2)\n" \
707 " for (var i = 1; i < 7; ++i) {\n" \
708 " var cmp = (2 != i)\n" \
709 " ? parseInt(dp1[i]) - parseInt(dp2[i])\n" \
710 " : li_mon_num(dp1[2]) - li_mon_num(dp2[2]);\n" \
711 " if (0 != cmp) return cmp;\n" \
712 " }\n" \
713 " return 0;\n" \
714 "}\n" \
715 "\n" \
716 "function sortfn_then_by_name(a,b,sort_column) {\n" \
717 " if (sort_column == name_column || sort_column == type_column) {\n" \
718 " var ad = (a.cells[type_column].innerHTML === 'Directory');\n" \
719 " var bd = (b.cells[type_column].innerHTML === 'Directory');\n" \
720 " if (ad != bd) return (ad ? -1 : 1);\n" \
721 " }\n" \
722 " var at = get_inner_text(a.cells[sort_column]);\n" \
723 " var bt = get_inner_text(b.cells[sort_column]);\n" \
724 " var cmp;\n" \
725 " if (sort_column == name_column) {\n" \
726 " if (at == '../') return -1;\n" \
727 " if (bt == '../') return 1;\n" \
728 " }\n" \
729 " if (a.cells[sort_column].className == 'int') {\n" \
730 " cmp = parseInt(at)-parseInt(bt);\n" \
731 " } else if (sort_column == date_column) {\n" \
732 " var ad = isdigit(at.substr(0,1));\n" \
733 " var bd = isdigit(bt.substr(0,1));\n" \
734 " if (ad != bd) return (!ad ? -1 : 1);\n" \
735 " cmp = li_date_cmp(at,bt);\n" \
736 " } else if (sort_column == size_column) {\n" \
737 " var ai = parseInt(at, 10) * unit_multiplier(at.substr(-1,1));\n" \
738 " var bi = parseInt(bt, 10) * unit_multiplier(bt.substr(-1,1));\n" \
739 " if (at.substr(0,1) == '-') ai = -1;\n" \
740 " if (bt.substr(0,1) == '-') bi = -1;\n" \
741 " cmp = ai - bi;\n" \
742 " } else {\n" \
743 " cmp = at.toLocaleUpperCase().localeCompare(bt.toLocaleUpperCase());\n" \
744 " if (0 != cmp) return cmp;\n" \
745 " cmp = at.localeCompare(bt);\n" \
746 " }\n" \
747 " if (0 != cmp || sort_column == name_column) return cmp;\n" \
748 " return sortfn_then_by_name(a,b,name_column);\n" \
749 "}\n" \
750 "\n" \
751 "function sortfn(a,b) {\n" \
752 " return sortfn_then_by_name(a,b,click_column);\n" \
753 "}\n" \
754 "\n" \
755 "function resort(lnk) {\n" \
756 " var span = lnk.childNodes[1];\n" \
757 " var table = lnk.parentNode.parentNode.parentNode.parentNode;\n" \
758 " var rows = new Array();\n" \
759 " for (var j=1;j<table.rows.length;j++)\n" \
760 " rows[j-1] = table.rows[j];\n" \
761 " click_column = lnk.parentNode.cellIndex;\n" \
762 " rows.sort(sortfn);\n" \
763 "\n" \
764 " if (prev_span != null) prev_span.innerHTML = '';\n" \
765 " if (span.getAttribute('sortdir')=='down') {\n" \
766 " span.innerHTML = '↑';\n" \
767 " span.setAttribute('sortdir','up');\n" \
768 " rows.reverse();\n" \
769 " } else {\n" \
770 " span.innerHTML = '↓';\n" \
771 " span.setAttribute('sortdir','down');\n" \
772 " }\n" \
773 " for (var i=0;i<rows.length;i++)\n" \
774 " table.tBodies[0].appendChild(rows[i]);\n" \
775 " prev_span = span;\n" \
776 "}\n";
777
778 /* portions copied from mod_dirlist (lighttpd2) */
779 static const char js_simple_table_init_sort[] = \
780 "\n" \
781 "function init_sort(init_sort_column, ascending) {\n" \
782 " var tables = document.getElementsByTagName(\"table\");\n" \
783 " for (var i = 0; i < tables.length; i++) {\n" \
784 " var table = tables[i];\n" \
785 " //var c = table.getAttribute(\"class\")\n" \
786 " //if (-1 != c.split(\" \").indexOf(\"sort\")) {\n" \
787 " var row = table.rows[0].cells;\n" \
788 " for (var j = 0; j < row.length; j++) {\n" \
789 " var n = row[j];\n" \
790 " if (n.childNodes.length == 1 && n.childNodes[0].nodeType == 3) {\n" \
791 " var link = document.createElement(\"a\");\n" \
792 " var title = n.childNodes[0].nodeValue.replace(/:$/, \"\");\n" \
793 " link.appendChild(document.createTextNode(title));\n" \
794 " link.setAttribute(\"href\", \"#\");\n" \
795 " link.setAttribute(\"class\", \"sortheader\");\n" \
796 " link.setAttribute(\"onclick\", \"resort(this);return false;\");\n" \
797 " var arrow = document.createElement(\"span\");\n" \
798 " arrow.setAttribute(\"class\", \"sortarrow\");\n" \
799 " arrow.appendChild(document.createTextNode(\":\"));\n" \
800 " link.appendChild(arrow)\n" \
801 " n.replaceChild(link, n.firstChild);\n" \
802 " }\n" \
803 " }\n" \
804 " var lnk = row[init_sort_column].firstChild;\n" \
805 " if (ascending) {\n" \
806 " var span = lnk.childNodes[1];\n" \
807 " span.setAttribute('sortdir','down');\n" \
808 " }\n" \
809 " resort(lnk);\n" \
810 " //}\n" \
811 " }\n" \
812 "}\n" \
813 "\n" \
814 "function init_sort_from_query() {\n" \
815 " var urlParams = new URLSearchParams(location.search);\n" \
816 " var c = 0;\n" \
817 " var o = 0;\n" \
818 " switch (urlParams.get('C')) {\n" \
819 " case \"N\": c=0; break;\n" \
820 " case \"M\": c=1; break;\n" \
821 " case \"S\": c=2; break;\n" \
822 " case \"T\":\n" \
823 " case \"D\": c=3; break;\n" \
824 " }\n" \
825 " switch (urlParams.get('O')) {\n" \
826 " case \"A\": o=1; break;\n" \
827 " case \"D\": o=0; break;\n" \
828 " }\n" \
829 " init_sort(c,o);\n" \
830 "}\n" \
831 "init_sort_from_query();\n";
832
833
http_dirlist_append_js_table_resort(buffer * const b)834 static void http_dirlist_append_js_table_resort (buffer * const b) {
835 struct const_iovec iov[] = {
836 { CONST_STR_LEN("\n<script type=\"text/javascript\">\n// <!--\n\n") }
837 ,{ CONST_STR_LEN(js_simple_table_resort) }
838 ,{ CONST_STR_LEN(js_simple_table_init_sort) }
839 ,{ CONST_STR_LEN("\n// -->\n</script>\n\n") }
840 };
841 buffer_append_iovec(b, iov, sizeof(iov)/sizeof(*iov));
842 }
843
http_list_directory_header(request_st * const r,const handler_ctx * const p)844 static void http_list_directory_header(request_st * const r, const handler_ctx * const p) {
845
846 chunkqueue * const cq = &r->write_queue;
847 if (p->conf.auto_layout) {
848 buffer * const out = chunkqueue_append_buffer_open(cq);
849 buffer_append_string_len(out, CONST_STR_LEN(
850 "<!DOCTYPE html>\n"
851 "<html>\n"
852 "<head>\n"
853 ));
854 if (p->conf.encoding) {
855 buffer_append_str3(out,
856 CONST_STR_LEN("<meta charset=\""),
857 BUF_PTR_LEN(p->conf.encoding),
858 CONST_STR_LEN("\">\n"));
859 }
860 buffer_append_string_len(out, CONST_STR_LEN("<title>Index of "));
861 buffer_append_string_encoded(out, BUF_PTR_LEN(&r->uri.path), ENCODING_MINIMAL_XML);
862 buffer_append_string_len(out, CONST_STR_LEN("</title>\n"));
863
864 if (p->conf.external_css) {
865 buffer_append_str3(out,
866 CONST_STR_LEN("<meta name=\"viewport\" content=\"initial-scale=1\">"
867 "<link rel=\"stylesheet\" type=\"text/css\" href=\""),
868 BUF_PTR_LEN(p->conf.external_css),
869 CONST_STR_LEN("\">\n"));
870 } else {
871 buffer_append_string_len(out, CONST_STR_LEN(
872 "<style type=\"text/css\">\n"
873 "a, a:active {text-decoration: none; color: blue;}\n"
874 "a:visited {color: #48468F;}\n"
875 "a:hover, a:focus {text-decoration: underline; color: red;}\n"
876 "body {background-color: #F5F5F5;}\n"
877 "h2 {margin-bottom: 12px;}\n"
878 "table {margin-left: 12px;}\n"
879 "th, td {"
880 " font: 90% monospace;"
881 " text-align: left;"
882 "}\n"
883 "th {"
884 " font-weight: bold;"
885 " padding-right: 14px;"
886 " padding-bottom: 3px;"
887 "}\n"
888 "td {padding-right: 14px;}\n"
889 "td.s, th.s {text-align: right;}\n"
890 "div.list {"
891 " background-color: white;"
892 " border-top: 1px solid #646464;"
893 " border-bottom: 1px solid #646464;"
894 " padding-top: 10px;"
895 " padding-bottom: 14px;"
896 "}\n"
897 "div.foot {"
898 " font: 90% monospace;"
899 " color: #787878;"
900 " padding-top: 4px;"
901 "}\n"
902 "</style>\n"
903 ));
904 }
905
906 buffer_append_string_len(out, CONST_STR_LEN("</head>\n<body>\n"));
907 chunkqueue_append_buffer_commit(cq);
908 }
909
910 if (p->conf.show_header) {
911 http_list_directory_include_file(r, p, 1);/*0 for readme; 1 for header*/
912 }
913
914 buffer * const out = chunkqueue_append_buffer_open(cq);
915 buffer_append_string_len(out, CONST_STR_LEN("<h2>Index of "));
916 buffer_append_string_encoded(out, BUF_PTR_LEN(&r->uri.path), ENCODING_MINIMAL_XML);
917 buffer_append_string_len(out, CONST_STR_LEN(
918 "</h2>\n"
919 "<div class=\"list\">\n"
920 "<table summary=\"Directory Listing\" cellpadding=\"0\" cellspacing=\"0\">\n"
921 "<thead>"
922 "<tr>"
923 "<th class=\"n\">Name</th>"
924 "<th class=\"m\">Last Modified</th>"
925 "<th class=\"s\">Size</th>"
926 "<th class=\"t\">Type</th>"
927 "</tr>"
928 "</thead>\n"
929 "<tbody>\n"
930 ));
931 if (!buffer_is_equal_string(&r->uri.path, CONST_STR_LEN("/"))) {
932 buffer_append_string_len(out, CONST_STR_LEN(
933 "<tr class=\"d\">"
934 "<td class=\"n\"><a href=\"../\">..</a>/</td>"
935 "<td class=\"m\"> </td>"
936 "<td class=\"s\">- </td>"
937 "<td class=\"t\">Directory</td>"
938 "</tr>\n"
939 ));
940 }
941 chunkqueue_append_buffer_commit(cq);
942 }
943
http_list_directory_footer(request_st * const r,const handler_ctx * const p)944 static void http_list_directory_footer(request_st * const r, const handler_ctx * const p) {
945
946 chunkqueue * const cq = &r->write_queue;
947 chunkqueue_append_mem(cq, CONST_STR_LEN(
948 "</tbody>\n"
949 "</table>\n"
950 "</div>\n"
951 ));
952
953 if (p->conf.show_readme) {
954 http_list_directory_include_file(r, p, 0);/*0 for readme; 1 for header*/
955 }
956
957 if (p->conf.auto_layout) {
958 buffer * const out = chunkqueue_append_buffer_open(cq);
959 const buffer * const footer =
960 p->conf.set_footer
961 ? p->conf.set_footer
962 : r->conf.server_tag
963 ? r->conf.server_tag
964 : NULL;
965 if (footer)
966 buffer_append_str3(out,
967 CONST_STR_LEN("<div class=\"foot\">"),
968 BUF_PTR_LEN(footer),
969 CONST_STR_LEN("</div>\n"));
970
971 if (p->conf.external_js)
972 buffer_append_str3(out,
973 CONST_STR_LEN("<script type=\"text/javascript\" src=\""),
974 BUF_PTR_LEN(p->conf.external_js),
975 CONST_STR_LEN("\"></script>\n"));
976 else
977 http_dirlist_append_js_table_resort(out);
978
979 buffer_append_string_len(out, CONST_STR_LEN(
980 "</body>\n"
981 "</html>\n"
982 ));
983 chunkqueue_append_buffer_commit(cq);
984 }
985 }
986
http_open_directory(request_st * const r,handler_ctx * const hctx)987 static int http_open_directory(request_st * const r, handler_ctx * const hctx) {
988 const uint32_t dlen = buffer_clen(&r->physical.path);
989 #if defined __WIN32
990 hctx->name_max = FILENAME_MAX;
991 #else
992 #ifndef PATH_MAX
993 #define PATH_MAX 4096
994 #endif
995 /* allocate based on PATH_MAX rather than pathconf() to get _PC_NAME_MAX */
996 hctx->name_max = PATH_MAX - dlen - 1;
997 #endif
998 hctx->path = ck_malloc(dlen + hctx->name_max + 1);
999 memcpy(hctx->path, r->physical.path.ptr, dlen+1);
1000 #if defined(HAVE_XATTR) || defined(HAVE_EXTATTR) || !defined(_ATFILE_SOURCE)
1001 hctx->path_file = hctx->path + dlen;
1002 #endif
1003
1004 #ifndef _ATFILE_SOURCE /*(not using fdopendir unless _ATFILE_SOURCE)*/
1005 hctx->dfd = -1;
1006 hctx->dp = opendir(hctx->path);
1007 #else
1008 hctx->dfd = fdevent_open_dirname(hctx->path, r->conf.follow_symlink);
1009 hctx->dp = (hctx->dfd >= 0) ? fdopendir(hctx->dfd) : NULL;
1010 #endif
1011 if (NULL == hctx->dp) {
1012 log_perror(r->conf.errh, __FILE__, __LINE__, "opendir %s", hctx->path);
1013 if (hctx->dfd >= 0) {
1014 close(hctx->dfd);
1015 hctx->dfd = -1;
1016 }
1017 return -1;
1018 }
1019
1020 if (hctx->conf.json) return 0;
1021
1022 dirls_list_t * const dirs = &hctx->dirs;
1023 dirls_list_t * const files = &hctx->files;
1024 dirs->ent = NULL;
1025 dirs->used = 0;
1026 files->ent = NULL;
1027 files->used = 0;
1028
1029 return 0;
1030 }
1031
http_read_directory(handler_ctx * const p)1032 static int http_read_directory(handler_ctx * const p) {
1033 struct dirent *dent;
1034 const int hide_dotfiles = p->conf.hide_dot_files;
1035 const uint32_t name_max = p->name_max;
1036 struct stat st;
1037 int count = -1;
1038 while (++count < DIRLIST_BATCH && (dent = readdir(p->dp)) != NULL) {
1039 const char * const d_name = dent->d_name;
1040 const uint32_t dsz = (uint32_t) _D_EXACT_NAMLEN(dent);
1041 if (d_name[0] == '.') {
1042 if (hide_dotfiles)
1043 continue;
1044 if (d_name[1] == '\0')
1045 continue;
1046 if (d_name[1] == '.' && d_name[2] == '\0')
1047 continue;
1048 }
1049
1050 if (p->conf.hide_readme_file
1051 && p->conf.show_readme
1052 && buffer_eq_slen(p->conf.show_readme, d_name, dsz))
1053 continue;
1054 if (p->conf.hide_header_file
1055 && p->conf.show_header
1056 && buffer_eq_slen(p->conf.show_header, d_name, dsz))
1057 continue;
1058
1059 /* compare d_name against excludes array
1060 * elements, skipping any that match.
1061 */
1062 if (p->conf.excludes
1063 && mod_dirlisting_exclude(p->conf.excludes, d_name, dsz))
1064 continue;
1065
1066 /* NOTE: the manual says, d_name is never more than NAME_MAX
1067 * so this should actually not be a buffer-overflow-risk
1068 */
1069 if (dsz > name_max) continue;
1070 #ifdef __COVERITY__
1071 /* For some reason, Coverity overlooks the strlen() performed
1072 * a few lines above and thinks memcpy() below might access
1073 * bytes beyond end of d_name[] with dsz+1 */
1074 force_assert(dsz < sizeof(dent->d_name));
1075 #endif
1076
1077 #ifndef _ATFILE_SOURCE
1078 memcpy(p->path_file, d_name, dsz + 1);
1079 if (stat(p->path, &st) != 0)
1080 continue;
1081 #else
1082 /*(XXX: follow symlinks, like stat(); not using AT_SYMLINK_NOFOLLOW) */
1083 if (0 != fstatat(p->dfd, d_name, &st, 0))
1084 continue; /* file *just* disappeared? */
1085 #endif
1086
1087 if (p->jb) { /* json output */
1088 if (__builtin_expect( (p->jcomma), 1))/*(to avoid excess comma)*/
1089 buffer_append_string_len(p->jb, CONST_STR_LEN(",{\"name\":\""));
1090 else {
1091 p->jcomma = 1;
1092 buffer_append_string_len(p->jb, CONST_STR_LEN( "{\"name\":\""));
1093 }
1094 buffer_append_bs_escaped_json(p->jb, d_name, dsz);
1095
1096 const char *t;
1097 size_t tlen;
1098 if (!S_ISDIR(st.st_mode)) {
1099 t = "\",\"type\":\"file\",\"size\":";
1100 tlen = sizeof("\",\"type\":\"file\",\"size\":")-1;
1101 }
1102 else {
1103 t = "\",\"type\":\"dir\",\"size\":";
1104 tlen = sizeof("\",\"type\":\"dir\",\"size\":")-1;
1105 }
1106 char sstr[LI_ITOSTRING_LENGTH];
1107 char mstr[LI_ITOSTRING_LENGTH];
1108 struct const_iovec iov[] = {
1109 { t, tlen }
1110 ,{ sstr, li_itostrn(sstr, sizeof(sstr), st.st_size) }
1111 ,{ CONST_STR_LEN(",\"mtime\":") }
1112 ,{ mstr, li_itostrn(mstr, sizeof(mstr), TIME64_CAST(st.st_mtime)) }
1113 ,{ CONST_STR_LEN("}") }
1114 };
1115 buffer_append_iovec(p->jb, iov, sizeof(iov)/sizeof(*iov));
1116 continue;
1117 }
1118
1119 dirls_list_t * const list = !S_ISDIR(st.st_mode) ? &p->files : &p->dirs;
1120 if (!(list->used & (DIRLIST_BLOB_SIZE-1)))
1121 ck_realloc_u32((void **)&list->ent, list->used,
1122 DIRLIST_BLOB_SIZE, sizeof(*list->ent));
1123 dirls_entry_t * const tmp = list->ent[list->used++] =
1124 (dirls_entry_t*) ck_malloc(sizeof(dirls_entry_t) + 1 + dsz);
1125 tmp->mtime = st.st_mtime;
1126 tmp->size = st.st_size;
1127 tmp->namelen = dsz;
1128 memcpy(DIRLIST_ENT_NAME(tmp), d_name, dsz + 1);
1129 }
1130 if (count == DIRLIST_BATCH)
1131 return HANDLER_WAIT_FOR_EVENT;
1132 closedir(p->dp);
1133 p->dp = NULL;
1134
1135 return HANDLER_FINISHED;
1136 }
1137
http_list_directory(request_st * const r,handler_ctx * const hctx)1138 static void http_list_directory(request_st * const r, handler_ctx * const hctx) {
1139 dirls_list_t * const dirs = &hctx->dirs;
1140 dirls_list_t * const files = &hctx->files;
1141 /*(note: sorting can be time consuming on large dirs (O(n log n))*/
1142 if (dirs->used) http_dirls_sort(dirs->ent, dirs->used);
1143 if (files->used) http_dirls_sort(files->ent, files->used);
1144
1145 char sizebuf[sizeof("999.9K")];
1146 struct tm tm;
1147
1148 /* generate large directory listings into tempfiles
1149 * (estimate approx 200-256 bytes of HTML per item; could be up to ~512) */
1150 chunkqueue * const cq = &r->write_queue;
1151 buffer * const tb = r->tmp_buf;
1152 buffer_clear(tb);
1153 buffer * const out = (dirs->used + files->used <= 256)
1154 ? chunkqueue_append_buffer_open(cq)
1155 : tb;
1156 buffer_clear(out);
1157
1158 /* directories */
1159 dirls_entry_t ** const dirs_ent = dirs->ent;
1160 for (uint32_t i = 0, used = dirs->used; i < used; ++i) {
1161 dirls_entry_t * const tmp = dirs_ent[i];
1162
1163 buffer_append_string_len(out, CONST_STR_LEN("<tr class=\"d\"><td class=\"n\"><a href=\""));
1164 buffer_append_string_encoded(out, DIRLIST_ENT_NAME(tmp), tmp->namelen, ENCODING_REL_URI_PART);
1165 buffer_append_string_len(out, CONST_STR_LEN("/\">"));
1166 buffer_append_string_encoded(out, DIRLIST_ENT_NAME(tmp), tmp->namelen, ENCODING_MINIMAL_XML);
1167 buffer_append_string_len(out, CONST_STR_LEN("</a>/</td><td class=\"m\">"));
1168 buffer_append_strftime(out, "%Y-%b-%d %T", localtime64_r(&tmp->mtime, &tm));
1169 buffer_append_string_len(out, CONST_STR_LEN("</td><td class=\"s\">- </td><td class=\"t\">Directory</td></tr>\n"));
1170
1171 if (buffer_string_space(out) < 256) {
1172 if (out == tb) {
1173 if (0 != chunkqueue_append_mem_to_tempfile(cq,
1174 BUF_PTR_LEN(out),
1175 r->conf.errh))
1176 break;
1177 buffer_clear(out);
1178 }
1179 }
1180 }
1181
1182 /* files */
1183 const array * const mimetypes = r->conf.mimetypes;
1184 dirls_entry_t ** const files_ent = files->ent;
1185 for (uint32_t i = 0, used = files->used; i < used; ++i) {
1186 dirls_entry_t * const tmp = files_ent[i];
1187 const buffer *content_type;
1188 #if defined(HAVE_XATTR) || defined(HAVE_EXTATTR) /*(pass full path)*/
1189 content_type = NULL;
1190 if (r->conf.use_xattr) {
1191 memcpy(hctx->path_file, DIRLIST_ENT_NAME(tmp), tmp->namelen + 1);
1192 content_type = stat_cache_mimetype_by_xattr(hctx->path);
1193 }
1194 if (NULL == content_type)
1195 #endif
1196 content_type = stat_cache_mimetype_by_ext(mimetypes, DIRLIST_ENT_NAME(tmp), tmp->namelen);
1197 if (NULL == content_type) {
1198 static const buffer octet_stream =
1199 { "application/octet-stream",
1200 sizeof("application/octet-stream"), 0 };
1201 content_type = &octet_stream;
1202 }
1203
1204 buffer_append_string_len(out, CONST_STR_LEN("<tr><td class=\"n\"><a href=\""));
1205 buffer_append_string_encoded(out, DIRLIST_ENT_NAME(tmp), tmp->namelen, ENCODING_REL_URI_PART);
1206 buffer_append_string_len(out, CONST_STR_LEN("\">"));
1207 buffer_append_string_encoded(out, DIRLIST_ENT_NAME(tmp), tmp->namelen, ENCODING_MINIMAL_XML);
1208 buffer_append_string_len(out, CONST_STR_LEN("</a></td><td class=\"m\">"));
1209 buffer_append_strftime(out, "%Y-%b-%d %T", localtime64_r(&tmp->mtime, &tm));
1210 size_t buflen =
1211 http_list_directory_sizefmt(sizebuf, sizeof(sizebuf), tmp->size);
1212 struct const_iovec iov[] = {
1213 { CONST_STR_LEN("</td><td class=\"s\">") }
1214 ,{ sizebuf, buflen }
1215 ,{ CONST_STR_LEN("</td><td class=\"t\">") }
1216 ,{ BUF_PTR_LEN(content_type) }
1217 ,{ CONST_STR_LEN("</td></tr>\n") }
1218 };
1219 buffer_append_iovec(out, iov, sizeof(iov)/sizeof(*iov));
1220
1221 if (buffer_string_space(out) < 256) {
1222 if (out == tb) {
1223 if (0 != chunkqueue_append_mem_to_tempfile(cq,
1224 BUF_PTR_LEN(out),
1225 r->conf.errh))
1226 break;
1227 buffer_clear(out);
1228 }
1229 }
1230 }
1231
1232 if (out == tb) {
1233 if (!buffer_is_blank(out)
1234 && 0 != chunkqueue_append_mem_to_tempfile(cq,
1235 BUF_PTR_LEN(out),
1236 r->conf.errh)) {
1237 /* ignore */
1238 }
1239 }
1240 else {
1241 chunkqueue_append_buffer_commit(cq);
1242 }
1243 }
1244
1245
mod_dirlisting_content_type(request_st * const r,const buffer * const encoding)1246 static void mod_dirlisting_content_type (request_st * const r, const buffer * const encoding) {
1247 buffer * const vb =
1248 http_header_response_set_ptr(r, HTTP_HEADER_CONTENT_TYPE,
1249 CONST_STR_LEN("Content-Type"));
1250 if (NULL == encoding)
1251 buffer_copy_string_len(vb, CONST_STR_LEN("text/html"));
1252 else
1253 buffer_append_str2(vb, CONST_STR_LEN("text/html;charset="),
1254 BUF_PTR_LEN(encoding));
1255 }
1256
1257
mod_dirlisting_response(request_st * const r,handler_ctx * const hctx)1258 static void mod_dirlisting_response (request_st * const r, handler_ctx * const hctx) {
1259 http_list_directory_header(r, hctx);
1260 http_list_directory(r, hctx);
1261 http_list_directory_footer(r, hctx);
1262 mod_dirlisting_content_type(r, hctx->conf.encoding);
1263
1264 r->resp_body_finished = 1;
1265 }
1266
1267
mod_dirlisting_json_append(request_st * const r,handler_ctx * const hctx,const int fin)1268 static void mod_dirlisting_json_append (request_st * const r, handler_ctx * const hctx, const int fin) {
1269 buffer * const jb = hctx->jb;
1270 if (fin)
1271 buffer_append_string_len(jb, CONST_STR_LEN("]}"));
1272 else if (buffer_clen(jb) < 16384-1024)
1273 return; /* aggregate bunches of entries, even if streaming response */
1274
1275 if (hctx->jfn) {
1276 if (__builtin_expect( (write_all(hctx->jfd, BUF_PTR_LEN(jb)) < 0), 0)) {
1277 /*(cleanup, cease caching if error occurs writing to cache file)*/
1278 unlink(hctx->jfn);
1279 free(hctx->jfn);
1280 hctx->jfn = NULL;
1281 close(hctx->jfd);
1282 hctx->jfd = -1;
1283 }
1284 /* Note: writing cache file is separate from the response so that if an
1285 * error occurs with cache, the response still proceeds. While this is
1286 * duplicative if the response is large enough to spill to temporary
1287 * files, it is expected that only very large directories will spill to
1288 * temporary files, and even then most responses will be less than 1 MB.
1289 * The cache path can be different from server.upload-dirs.
1290 * Note: since responses are not expected to be large, no effort is
1291 * currently made here to handle FDEVENT_STREAM_RESPONSE_BUFMIN and to
1292 * defer reading more from directory while data is sent to client */
1293 }
1294
1295 http_chunk_append_buffer(r, jb); /* clears jb */
1296 }
1297
1298
1299 SUBREQUEST_FUNC(mod_dirlisting_subrequest);
1300 REQUEST_FUNC(mod_dirlisting_reset);
1301 static handler_t mod_dirlisting_cache_check (request_st * const r, plugin_data * const p);
1302 __attribute_noinline__
1303 static void mod_dirlisting_cache_add (request_st * const r, handler_ctx * const hctx);
1304 __attribute_noinline__
1305 static void mod_dirlisting_cache_json_init (request_st * const r, handler_ctx * const hctx);
1306 __attribute_noinline__
1307 static void mod_dirlisting_cache_json (request_st * const r, handler_ctx * const hctx);
1308
1309
URIHANDLER_FUNC(mod_dirlisting_subrequest_start)1310 URIHANDLER_FUNC(mod_dirlisting_subrequest_start) {
1311 plugin_data *p = p_d;
1312
1313 if (NULL != r->handler_module) return HANDLER_GO_ON;
1314 if (!buffer_has_slash_suffix(&r->uri.path)) return HANDLER_GO_ON;
1315 if (!http_method_get_or_head(r->http_method)) return HANDLER_GO_ON;
1316 /* r->physical.path is non-empty for handle_subrequest_start */
1317 /*if (buffer_is_blank(&r->physical.path)) return HANDLER_GO_ON;*/
1318
1319 mod_dirlisting_patch_config(r, p);
1320
1321 if (!p->conf.dir_listing) return HANDLER_GO_ON;
1322
1323 if (r->conf.log_request_handling) {
1324 log_error(r->conf.errh, __FILE__, __LINE__,
1325 "-- handling the request as Dir-Listing");
1326 log_error(r->conf.errh, __FILE__, __LINE__,
1327 "URI : %s", r->uri.path.ptr);
1328 }
1329
1330 #if 0 /* redundant check; not necessary */
1331 /* r->physical.path is a dir since it ends in slash, or else
1332 * http_response_physical_path_check() would have redirected
1333 * before calling handle_subrequest_start */
1334 if (!stat_cache_path_isdir(&r->physical.path)) {
1335 if (errno == ENOTDIR)
1336 return HANDLER_GO_ON;
1337 log_perror(r->conf.errh,__FILE__,__LINE__,"%s",r->physical.path.ptr);
1338 r->http_status = 500;
1339 return HANDLER_FINISHED;
1340 }
1341 #endif
1342
1343 /* TODO: add option/mechanism to enable json output */
1344 #if 0 /* XXX: ??? might this be enabled accidentally by clients ??? */
1345 const buffer * const vb =
1346 http_header_request_get(r, HTTP_HEADER_ACCEPT, CONST_STR_LEN("Accept"));
1347 p->conf.json = (vb && strstr(vb->ptr, "application/json")); /*(coarse)*/
1348 #endif
1349
1350 if (p->conf.cache) {
1351 handler_t rc = mod_dirlisting_cache_check(r, p);
1352 if (rc != HANDLER_GO_ON)
1353 return rc;
1354 }
1355
1356 /* upper limit for dirlisting requests in progress (per lighttpd worker)
1357 * (attempt to avoid "livelock" scenarios or starvation of other requests)
1358 * (100 is still a high arbitrary limit;
1359 * and limit applies only to directories larger than DIRLIST_BATCH-2) */
1360 if (p->processing == dirlist_max_in_progress) {
1361 r->http_status = 503;
1362 http_header_response_set(r, HTTP_HEADER_OTHER,
1363 CONST_STR_LEN("Retry-After"),
1364 CONST_STR_LEN("2"));
1365 return HANDLER_FINISHED;
1366 }
1367
1368 handler_ctx * const hctx = mod_dirlisting_handler_ctx_init(p);
1369
1370 /* future: might implement a queue to limit max number of dirlisting
1371 * requests being serviced in parallel (increasing disk I/O), and if
1372 * caching is enabled, to avoid repeating the work on the same directory
1373 * in parallel. Could continue serving (expired) cached entry while
1374 * updating, but burst of requests on first access to dir would still
1375 * need to be handled.
1376 *
1377 * If queueing (not implemented), defer opening dir until pulled off
1378 * queue. Since joblist is per-connection, would need to handle single
1379 * request from queue even if multiple streams are queued on same
1380 * HTTP/2 connection. If queueing, must check for and remove from
1381 * queue in mod_dirlisting_reset() if request is still queued. */
1382
1383 if (0 != http_open_directory(r, hctx)) {
1384 /* dirlisting failed */
1385 r->http_status = 403;
1386 mod_dirlisting_handler_ctx_free(hctx);
1387 return HANDLER_FINISHED;
1388 }
1389 ++p->processing;
1390
1391 if (p->conf.json) {
1392 hctx->jfd = -1;
1393 hctx->jb = chunk_buffer_acquire();
1394 buffer_append_string_len(hctx->jb, CONST_STR_LEN("{["));
1395 if (p->conf.cache)
1396 mod_dirlisting_cache_json_init(r, hctx);
1397 http_header_response_set(r, HTTP_HEADER_CONTENT_TYPE,
1398 CONST_STR_LEN("Content-Type"),
1399 CONST_STR_LEN("application/json"));
1400 r->http_status = 200;
1401 r->resp_body_started = 1;
1402 }
1403
1404 r->plugin_ctx[p->id] = hctx;
1405 r->handler_module = p->self;
1406 return mod_dirlisting_subrequest(r, p);
1407 }
1408
1409
SUBREQUEST_FUNC(mod_dirlisting_subrequest)1410 SUBREQUEST_FUNC(mod_dirlisting_subrequest) {
1411 plugin_data * const p = p_d;
1412 handler_ctx * const hctx = r->plugin_ctx[p->id];
1413 if (NULL == hctx) return HANDLER_GO_ON; /*(should not happen)*/
1414
1415 handler_t rc = http_read_directory(hctx);
1416 switch (rc) {
1417 case HANDLER_FINISHED:
1418 if (hctx->jb) { /* (hctx->conf.json) */
1419 mod_dirlisting_json_append(r, hctx, 1);
1420 r->resp_body_finished = 1;
1421 if (hctx->jfn) /* (also (hctx->conf.cache) */
1422 mod_dirlisting_cache_json(r, hctx);
1423 }
1424 else {
1425 mod_dirlisting_response(r, hctx);
1426 if (hctx->conf.cache)
1427 mod_dirlisting_cache_add(r, hctx);
1428 }
1429 mod_dirlisting_reset(r, p); /*(release resources, including hctx)*/
1430 break;
1431 case HANDLER_WAIT_FOR_EVENT: /*(used here to mean 'yield')*/
1432 if (hctx->jb) /* (hctx->conf.json) */
1433 mod_dirlisting_json_append(r, hctx, 0);
1434 joblist_append(r->con);
1435 break;
1436 default:
1437 break;
1438 }
1439
1440 return rc;
1441 }
1442
1443
REQUEST_FUNC(mod_dirlisting_reset)1444 REQUEST_FUNC(mod_dirlisting_reset) {
1445 void ** const restrict dptr = &r->plugin_ctx[((plugin_data *)p_d)->id];
1446 if (*dptr) {
1447 --((plugin_data *)p_d)->processing;
1448 mod_dirlisting_handler_ctx_free(*dptr);
1449 *dptr = NULL;
1450 }
1451 return HANDLER_GO_ON;
1452 }
1453
1454
mod_dirlisting_cache_control(request_st * const r,unix_time64_t max_age)1455 static void mod_dirlisting_cache_control (request_st * const r, unix_time64_t max_age) {
1456 if (!light_btst(r->resp_htags, HTTP_HEADER_CACHE_CONTROL)) {
1457 buffer * const vb =
1458 http_header_response_set_ptr(r, HTTP_HEADER_CACHE_CONTROL,
1459 CONST_STR_LEN("Cache-Control"));
1460 buffer_append_string_len(vb, CONST_STR_LEN("max-age="));
1461 buffer_append_int(vb, max_age);
1462 }
1463 }
1464
1465
mod_dirlisting_cache_check(request_st * const r,plugin_data * const p)1466 static handler_t mod_dirlisting_cache_check (request_st * const r, plugin_data * const p) {
1467 /* optional: an external process can trigger a refresh by deleting the cache
1468 * entry when the external process detects (or initiates) changes to dir */
1469 buffer * const tb = r->tmp_buf;
1470 buffer_copy_path_len2(tb, BUF_PTR_LEN(p->conf.cache->path),
1471 BUF_PTR_LEN(&r->physical.path));
1472 buffer_append_string_len(tb, p->conf.json ? "dirlist.json" : "dirlist.html",
1473 sizeof("dirlist.html")-1);
1474 stat_cache_entry * const sce = stat_cache_get_entry_open(tb, 1);
1475 if (NULL == sce || sce->fd == -1)
1476 return HANDLER_GO_ON;
1477 const unix_time64_t max_age =
1478 TIME64_CAST(sce->st.st_mtime) + p->conf.cache->max_age - log_epoch_secs;
1479 if (max_age < 0)
1480 return HANDLER_GO_ON;
1481
1482 !p->conf.json
1483 ? mod_dirlisting_content_type(r, p->conf.encoding)
1484 : http_header_response_set(r, HTTP_HEADER_CONTENT_TYPE,
1485 CONST_STR_LEN("Content-Type"),
1486 CONST_STR_LEN("application/json"));
1487
1488 #if 0
1489 /*(XXX: ETag needs to be set for mod_deflate to potentially handle)*/
1490 /*(XXX: should ETag be created from cache file mtime or directory mtime?)*/
1491 const int follow_symlink = r->conf.follow_symlink;
1492 r->conf.follow_symlink = 1; /*(skip symlink checks into cache)*/
1493 http_response_send_file(r, sce->name, sce);
1494 r->conf.follow_symlink = follow_symlink;
1495 if (r->http_status < 400)
1496 return HANDLER_FINISHED;
1497 r->http_status = 0;
1498 #endif
1499
1500 /* Note: dirlist < 350 or so entries will generally trigger file
1501 * read into memory for dirlist < 32k, which will not be able to use
1502 * mod_deflate cache. Still, this is much more efficient than lots of
1503 * stat() calls to generate the dirlisting for each and every request */
1504 if (0 != http_chunk_append_file_ref(r, sce)) {
1505 http_header_response_unset(r, HTTP_HEADER_CONTENT_TYPE,
1506 CONST_STR_LEN("Content-Type"));
1507 http_response_body_clear(r, 0);
1508 return HANDLER_GO_ON;
1509 }
1510
1511 /* Cache-Control and ETag (also done in mod_dirlisting_cache_add())*/
1512 mod_dirlisting_cache_control(r, max_age);
1513 if (0 != r->conf.etag_flags) {
1514 const buffer *etag = stat_cache_etag_get(sce, r->conf.etag_flags);
1515 if (etag && !buffer_is_blank(etag))
1516 http_header_response_set(r, HTTP_HEADER_ETAG,
1517 CONST_STR_LEN("ETag"),
1518 BUF_PTR_LEN(etag));
1519 }
1520
1521 r->resp_body_finished = 1;
1522 return HANDLER_FINISHED;
1523 }
1524
1525
mod_dirlisting_write_cq(const int fd,chunkqueue * const cq,log_error_st * const errh)1526 static int mod_dirlisting_write_cq (const int fd, chunkqueue * const cq, log_error_st * const errh)
1527 {
1528 chunkqueue in;
1529 memset(&in, 0, sizeof(in));
1530 chunkqueue_append_chunkqueue(&in, cq);
1531 cq->bytes_in -= in.bytes_in;
1532 cq->bytes_out -= in.bytes_in;
1533
1534 /*(similar to mod_webdav.c:mod_webdav_write_cq(), but operates on two cqs)*/
1535 while (!chunkqueue_is_empty(&in)) {
1536 ssize_t wr = chunkqueue_write_chunk(fd, &in, errh);
1537 if (__builtin_expect( (wr > 0), 1))
1538 chunkqueue_steal(cq, &in, wr);
1539 else if (wr < 0) {
1540 /*(writing to tempfile failed; transfer remaining data back to cq)*/
1541 chunkqueue_append_chunkqueue(cq, &in);
1542 return 0;
1543 }
1544 else /*(wr == 0)*/
1545 chunkqueue_remove_finished_chunks(&in);
1546 }
1547 return 1;
1548 }
1549
1550
1551 #include <sys/stat.h> /* mkdir() */
1552 #include <sys/types.h>
1553 /*(similar to mod_deflate.c:mkdir_recursive(), but starts mid-path)*/
mkdir_recursive(char * dir,size_t off)1554 static int mkdir_recursive (char *dir, size_t off) {
1555 char *p = dir+off;
1556 if (*p != '/') {
1557 if (off && p[-1] == '/')
1558 --p;
1559 else {
1560 errno = ENOTDIR;
1561 return -1;
1562 }
1563 }
1564 do {
1565 *p = '\0';
1566 int rc = mkdir(dir, 0700);
1567 *p = '/';
1568 if (0 != rc && errno != EEXIST) return -1;
1569 } while ((p = strchr(p+1, '/')) != NULL);
1570 return 0;
1571 }
1572
1573
1574 __attribute_noinline__
mod_dirlisting_cache_add(request_st * const r,handler_ctx * const hctx)1575 static void mod_dirlisting_cache_add (request_st * const r, handler_ctx * const hctx) {
1576 #ifndef PATH_MAX
1577 #define PATH_MAX 4096
1578 #endif
1579 char oldpath[PATH_MAX];
1580 char newpath[PATH_MAX];
1581 buffer * const tb = r->tmp_buf;
1582 buffer_copy_path_len2(tb, BUF_PTR_LEN(hctx->conf.cache->path),
1583 BUF_PTR_LEN(&r->physical.path));
1584 if (!stat_cache_path_isdir(tb)
1585 && 0 != mkdir_recursive(tb->ptr, buffer_clen(hctx->conf.cache->path)))
1586 return;
1587 buffer_append_string_len(tb, CONST_STR_LEN("dirlist.html"));
1588 const size_t len = buffer_clen(tb);
1589 if (len + 7 >= PATH_MAX) return;
1590 memcpy(newpath, tb->ptr, len+1); /*(include '\0')*/
1591 buffer_append_string_len(tb, CONST_STR_LEN(".XXXXXX"));
1592 memcpy(oldpath, tb->ptr, len+7+1); /*(include '\0')*/
1593 const int fd = fdevent_mkostemp(oldpath, 0);
1594 if (fd < 0) return;
1595 if (mod_dirlisting_write_cq(fd, &r->write_queue, r->conf.errh)
1596 && 0 == fdevent_rename(oldpath, newpath)) {
1597 stat_cache_invalidate_entry(newpath, len);
1598 /* Cache-Control and ETag (also done in mod_dirlisting_cache_check())*/
1599 mod_dirlisting_cache_control(r, hctx->conf.cache->max_age);
1600 if (0 != r->conf.etag_flags) {
1601 struct stat st;
1602 if (0 == fstat(fd, &st)) {
1603 buffer * const vb =
1604 http_header_response_set_ptr(r, HTTP_HEADER_ETAG,
1605 CONST_STR_LEN("ETag"));
1606 http_etag_create(vb, &st, r->conf.etag_flags);
1607 }
1608 }
1609 }
1610 else
1611 unlink(oldpath);
1612 close(fd);
1613 }
1614
1615
1616 __attribute_noinline__
mod_dirlisting_cache_json_init(request_st * const r,handler_ctx * const hctx)1617 static void mod_dirlisting_cache_json_init (request_st * const r, handler_ctx * const hctx) {
1618 #ifndef PATH_MAX
1619 #define PATH_MAX 4096
1620 #endif
1621 buffer * const tb = r->tmp_buf;
1622 buffer_copy_path_len2(tb, BUF_PTR_LEN(hctx->conf.cache->path),
1623 BUF_PTR_LEN(&r->physical.path));
1624 if (!stat_cache_path_isdir(tb)
1625 && 0 != mkdir_recursive(tb->ptr, buffer_clen(hctx->conf.cache->path)))
1626 return;
1627 buffer_append_string_len(tb, CONST_STR_LEN("dirlist.json.XXXXXX"));
1628 const int fd = fdevent_mkostemp(tb->ptr, 0);
1629 if (fd < 0) return;
1630 hctx->jfn_len = buffer_clen(tb);
1631 hctx->jfd = fd;
1632 hctx->jfn = ck_malloc(hctx->jfn_len+1);
1633 memcpy(hctx->jfn, tb->ptr, hctx->jfn_len+1); /*(include '\0')*/
1634 }
1635
1636
1637 __attribute_noinline__
mod_dirlisting_cache_json(request_st * const r,handler_ctx * const hctx)1638 static void mod_dirlisting_cache_json (request_st * const r, handler_ctx * const hctx) {
1639 #ifndef PATH_MAX
1640 #define PATH_MAX 4096
1641 #endif
1642 UNUSED(r);
1643 char newpath[PATH_MAX];
1644 const size_t len = hctx->jfn_len - 7; /*(-7 for .XXXXXX)*/
1645 force_assert(len < PATH_MAX);
1646 memcpy(newpath, hctx->jfn, len);
1647 newpath[len] = '\0';
1648 if (0 == fdevent_rename(hctx->jfn, newpath))
1649 stat_cache_invalidate_entry(newpath, len);
1650 else
1651 unlink(hctx->jfn);
1652 free(hctx->jfn);
1653 hctx->jfn = NULL;
1654 }
1655
1656
1657 __attribute_cold__
1658 int mod_dirlisting_plugin_init(plugin *p);
mod_dirlisting_plugin_init(plugin * p)1659 int mod_dirlisting_plugin_init(plugin *p) {
1660 p->version = LIGHTTPD_VERSION_ID;
1661 p->name = "dirlisting";
1662
1663 p->init = mod_dirlisting_init;
1664 p->handle_subrequest_start = mod_dirlisting_subrequest_start;
1665 p->handle_subrequest = mod_dirlisting_subrequest;
1666 p->handle_request_reset = mod_dirlisting_reset;
1667 p->set_defaults = mod_dirlisting_set_defaults;
1668 p->cleanup = mod_dirlisting_free;
1669
1670 return 0;
1671 }
1672