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