xref: /lighttpd1.4/src/mod_dirlisting.c (revision 66698469)
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_header.h"
22 #include "keyvalue.h"
23 #include "response.h"
24 
25 #include "plugin.h"
26 
27 #include "stat_cache.h"
28 
29 #include <errno.h>
30 #include <stdlib.h>
31 #include <string.h>
32 #include <dirent.h>
33 #include <fcntl.h>
34 #include <unistd.h>
35 
36 #ifdef AT_FDCWD
37 #ifndef _ATFILE_SOURCE
38 #define _ATFILE_SOURCE
39 #endif
40 #endif
41 
42 #ifndef _D_EXACT_NAMLEN
43 #ifdef _DIRENT_HAVE_D_NAMLEN
44 #define _D_EXACT_NAMLEN(d) ((d)->d_namlen)
45 #else
46 #define _D_EXACT_NAMLEN(d) (strlen ((d)->d_name))
47 #endif
48 #endif
49 
50 /**
51  * this is a dirlisting for a lighttpd plugin
52  *
53  * Notes:
54  * - mod_dirlisting is a dir list implementation.  One size does not fit all.
55  *   mod_dirlisting aims to be somewhat flexible, but if special behavior
56  *   is needed, use custom CGI/FastCGI/SCGI to handle dir listing instead.
57  *   - backend daemon could implement custom caching
58  *   - backend daemon could monitor directory for changes (e.g. inotify)
59  *   - backend daemon or scripts could trigger when directory is modified
60  *     and could regenerate index.html (and mod_indexfile could be used
61  *     instead of mod_dirlisting)
62  * - basic alphabetical sorting (in C locale) is done on server side
63  *   in case client does not execute javascript
64  *   (otherwise, response could be streamed, which is not done)
65  *      (possible future dir-listing.* config option)
66  * - reading entire directory into memory for sorting large directory
67  *   can lead to large memory usage if many simultaneous requests occur
68  */
69 
70 struct dirlist_cache {
71 	int32_t max_age;
72 	buffer *path;
73 };
74 
75 typedef struct {
76 	char dir_listing;
77 	char json;
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 	unix_time64_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 	buffer *jb;
127 	int jcomma;
128 	int jfd;
129 	char *jfn;
130 	uint32_t jfn_len;
131 	plugin_config conf;
132 } handler_ctx;
133 
134 #define DIRLIST_BATCH 32
135 static int dirlist_max_in_progress;
136 
137 
138 static handler_ctx * mod_dirlisting_handler_ctx_init (plugin_data * const p) {
139     handler_ctx *hctx = calloc(1, sizeof(*hctx));
140     force_assert(hctx);
141     memcpy(&hctx->conf, &p->conf, sizeof(plugin_config));
142     return hctx;
143 }
144 
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 
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 = calloc(1, sizeof(struct dirlist_cache));
213     force_assert(cache);
214     cache->max_age = max_age;
215     cache->path = path;
216     return cache;
217 }
218 
219 
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 
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 
262 INIT_FUNC(mod_dirlisting_init) {
263     return calloc(1, sizeof(plugin_data));
264 }
265 
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 
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 
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 
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 
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 */
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  */
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 
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 = '&uarr;';\n" \
767 "  span.setAttribute('sortdir','up');\n" \
768 "  rows.reverse();\n" \
769 " } else {\n" \
770 "  span.innerHTML = '&darr;';\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 
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 
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\">&nbsp;</td>"
936 			"<td class=\"s\">- &nbsp;</td>"
937 			"<td class=\"t\">Directory</td>"
938 		"</tr>\n"
939 		));
940 	}
941 	chunkqueue_append_buffer_commit(cq);
942 }
943 
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 
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 = malloc(dlen + hctx->name_max + 1);
999     force_assert(NULL != hctx->path);
1000     memcpy(hctx->path, r->physical.path.ptr, dlen+1);
1001   #if defined(HAVE_XATTR) || defined(HAVE_EXTATTR) || !defined(_ATFILE_SOURCE)
1002     hctx->path_file = hctx->path + dlen;
1003   #endif
1004 
1005   #ifndef _ATFILE_SOURCE /*(not using fdopendir unless _ATFILE_SOURCE)*/
1006     hctx->dfd = -1;
1007     hctx->dp = opendir(hctx->path);
1008   #else
1009     hctx->dfd = fdevent_open_dirname(hctx->path, r->conf.follow_symlink);
1010     hctx->dp = (hctx->dfd >= 0) ? fdopendir(hctx->dfd) : NULL;
1011   #endif
1012     if (NULL == hctx->dp) {
1013         log_perror(r->conf.errh, __FILE__, __LINE__, "opendir %s", hctx->path);
1014         if (hctx->dfd >= 0) {
1015             close(hctx->dfd);
1016             hctx->dfd = -1;
1017         }
1018         return -1;
1019     }
1020 
1021     if (hctx->conf.json) return 0;
1022 
1023     dirls_list_t * const dirs = &hctx->dirs;
1024     dirls_list_t * const files = &hctx->files;
1025     dirs->ent   =
1026       (dirls_entry_t**) malloc(sizeof(dirls_entry_t*) * DIRLIST_BLOB_SIZE);
1027     force_assert(dirs->ent);
1028     dirs->size  = DIRLIST_BLOB_SIZE;
1029     dirs->used  = 0;
1030     files->ent  =
1031       (dirls_entry_t**) malloc(sizeof(dirls_entry_t*) * DIRLIST_BLOB_SIZE);
1032     force_assert(files->ent);
1033     files->size = DIRLIST_BLOB_SIZE;
1034     files->used = 0;
1035 
1036     return 0;
1037 }
1038 
1039 static int http_read_directory(handler_ctx * const p) {
1040 	struct dirent *dent;
1041 	const int hide_dotfiles = p->conf.hide_dot_files;
1042 	const uint32_t name_max = p->name_max;
1043 	struct stat st;
1044 	int count = -1;
1045 	while (++count < DIRLIST_BATCH && (dent = readdir(p->dp)) != NULL) {
1046 		const char * const d_name = dent->d_name;
1047 		const uint32_t dsz = (uint32_t) _D_EXACT_NAMLEN(dent);
1048 		if (d_name[0] == '.') {
1049 			if (hide_dotfiles)
1050 				continue;
1051 			if (d_name[1] == '\0')
1052 				continue;
1053 			if (d_name[1] == '.' && d_name[2] == '\0')
1054 				continue;
1055 		}
1056 
1057 		if (p->conf.hide_readme_file
1058 		    && p->conf.show_readme
1059 		    && buffer_eq_slen(p->conf.show_readme, d_name, dsz))
1060 			continue;
1061 		if (p->conf.hide_header_file
1062 		    && p->conf.show_header
1063 		    && buffer_eq_slen(p->conf.show_header, d_name, dsz))
1064 			continue;
1065 
1066 		/* compare d_name against excludes array
1067 		 * elements, skipping any that match.
1068 		 */
1069 		if (p->conf.excludes
1070 		    && mod_dirlisting_exclude(p->conf.excludes, d_name, dsz))
1071 			continue;
1072 
1073 		/* NOTE: the manual says, d_name is never more than NAME_MAX
1074 		 *       so this should actually not be a buffer-overflow-risk
1075 		 */
1076 		if (dsz > name_max) continue;
1077 	  #ifdef __COVERITY__
1078 		/* For some reason, Coverity overlooks the strlen() performed
1079 		 * a few lines above and thinks memcpy() below might access
1080 		 * bytes beyond end of d_name[] with dsz+1 */
1081 		force_assert(dsz < sizeof(dent->d_name));
1082 	  #endif
1083 
1084 	  #ifndef _ATFILE_SOURCE
1085 		memcpy(p->path_file, d_name, dsz + 1);
1086 		if (stat(p->path, &st) != 0)
1087 			continue;
1088 	  #else
1089 		/*(XXX: follow symlinks, like stat(); not using AT_SYMLINK_NOFOLLOW) */
1090 		if (0 != fstatat(p->dfd, d_name, &st, 0))
1091 			continue; /* file *just* disappeared? */
1092 	  #endif
1093 
1094 		if (p->jb) { /* json output */
1095 			if (__builtin_expect( (p->jcomma), 1))/*(to avoid excess comma)*/
1096 				buffer_append_string_len(p->jb, CONST_STR_LEN(",{\"name\":\""));
1097 			else {
1098 				p->jcomma = 1;
1099 				buffer_append_string_len(p->jb, CONST_STR_LEN( "{\"name\":\""));
1100 			}
1101 			buffer_append_bs_escaped(p->jb, d_name, dsz, BS_ESCAPE_JSON);
1102 
1103 			const char *t;
1104 			size_t tlen;
1105 			if (!S_ISDIR(st.st_mode)) {
1106 				t =           "\",\"type\":\"file\",\"size\":";
1107 				tlen = sizeof("\",\"type\":\"file\",\"size\":")-1;
1108 			}
1109 			else {
1110 				t =           "\",\"type\":\"dir\",\"size\":";
1111 				tlen = sizeof("\",\"type\":\"dir\",\"size\":")-1;
1112 			}
1113 			char sstr[LI_ITOSTRING_LENGTH];
1114 			char mstr[LI_ITOSTRING_LENGTH];
1115 			struct const_iovec iov[] = {
1116 			  { t, tlen }
1117 			 ,{ sstr, li_itostrn(sstr, sizeof(sstr), st.st_size) }
1118 			 ,{ CONST_STR_LEN(",\"mtime\":") }
1119 			 ,{ mstr, li_itostrn(mstr, sizeof(mstr), TIME64_CAST(st.st_mtime)) }
1120 			 ,{ CONST_STR_LEN("}") }
1121 			};
1122 			buffer_append_iovec(p->jb, iov, sizeof(iov)/sizeof(*iov));
1123 			continue;
1124 		}
1125 
1126 		dirls_list_t * const list = !S_ISDIR(st.st_mode) ? &p->files : &p->dirs;
1127 		if (list->used == list->size) {
1128 			list->size += DIRLIST_BLOB_SIZE;
1129 			list->ent   = (dirls_entry_t**) realloc(list->ent, sizeof(dirls_entry_t*) * list->size);
1130 			force_assert(list->ent);
1131 		}
1132 		dirls_entry_t * const tmp = list->ent[list->used++] =
1133 		  (dirls_entry_t*) malloc(sizeof(dirls_entry_t) + 1 + dsz);
1134 		force_assert(tmp);
1135 		tmp->mtime = st.st_mtime;
1136 		tmp->size  = st.st_size;
1137 		tmp->namelen = dsz;
1138 		memcpy(DIRLIST_ENT_NAME(tmp), d_name, dsz + 1);
1139 	}
1140 	if (count == DIRLIST_BATCH)
1141 		return HANDLER_WAIT_FOR_EVENT;
1142 	closedir(p->dp);
1143 	p->dp = NULL;
1144 
1145 	return HANDLER_FINISHED;
1146 }
1147 
1148 static void http_list_directory(request_st * const r, handler_ctx * const hctx) {
1149 	dirls_list_t * const dirs = &hctx->dirs;
1150 	dirls_list_t * const files = &hctx->files;
1151 	/*(note: sorting can be time consuming on large dirs (O(n log n))*/
1152 	if (dirs->used) http_dirls_sort(dirs->ent, dirs->used);
1153 	if (files->used) http_dirls_sort(files->ent, files->used);
1154 
1155 	char sizebuf[sizeof("999.9K")];
1156 	struct tm tm;
1157 
1158 	/* generate large directory listings into tempfiles
1159 	 * (estimate approx 200-256 bytes of HTML per item; could be up to ~512) */
1160 	chunkqueue * const cq = &r->write_queue;
1161 	buffer * const tb = r->tmp_buf;
1162 	buffer_clear(tb);
1163 	buffer * const out = (dirs->used + files->used <= 256)
1164 	  ? chunkqueue_append_buffer_open(cq)
1165 	  : tb;
1166 	buffer_clear(out);
1167 
1168 	/* directories */
1169 	dirls_entry_t ** const dirs_ent = dirs->ent;
1170 	for (uint32_t i = 0, used = dirs->used; i < used; ++i) {
1171 		dirls_entry_t * const tmp = dirs_ent[i];
1172 
1173 		buffer_append_string_len(out, CONST_STR_LEN("<tr class=\"d\"><td class=\"n\"><a href=\""));
1174 		buffer_append_string_encoded(out, DIRLIST_ENT_NAME(tmp), tmp->namelen, ENCODING_REL_URI_PART);
1175 		buffer_append_string_len(out, CONST_STR_LEN("/\">"));
1176 		buffer_append_string_encoded(out, DIRLIST_ENT_NAME(tmp), tmp->namelen, ENCODING_MINIMAL_XML);
1177 		buffer_append_string_len(out, CONST_STR_LEN("</a>/</td><td class=\"m\">"));
1178 		buffer_append_strftime(out, "%Y-%b-%d %T", localtime64_r(&tmp->mtime, &tm));
1179 		buffer_append_string_len(out, CONST_STR_LEN("</td><td class=\"s\">- &nbsp;</td><td class=\"t\">Directory</td></tr>\n"));
1180 
1181 		if (buffer_string_space(out) < 256) {
1182 			if (out == tb) {
1183 				if (0 != chunkqueue_append_mem_to_tempfile(cq,
1184 				                                           BUF_PTR_LEN(out),
1185 				                                           r->conf.errh))
1186 					break;
1187 				buffer_clear(out);
1188 			}
1189 		}
1190 	}
1191 
1192 	/* files */
1193 	const array * const mimetypes = r->conf.mimetypes;
1194 	dirls_entry_t ** const files_ent = files->ent;
1195 	for (uint32_t i = 0, used = files->used; i < used; ++i) {
1196 		dirls_entry_t * const tmp = files_ent[i];
1197 		const buffer *content_type;
1198 	  #if defined(HAVE_XATTR) || defined(HAVE_EXTATTR) /*(pass full path)*/
1199 		content_type = NULL;
1200 		if (r->conf.use_xattr) {
1201 			memcpy(hctx->path_file, DIRLIST_ENT_NAME(tmp), tmp->namelen + 1);
1202 			content_type = stat_cache_mimetype_by_xattr(hctx->path);
1203 		}
1204 		if (NULL == content_type)
1205 	  #endif
1206 			content_type = stat_cache_mimetype_by_ext(mimetypes, DIRLIST_ENT_NAME(tmp), tmp->namelen);
1207 		if (NULL == content_type) {
1208 			static const buffer octet_stream =
1209 			  { "application/octet-stream",
1210 			    sizeof("application/octet-stream"), 0 };
1211 			content_type = &octet_stream;
1212 		}
1213 
1214 		buffer_append_string_len(out, CONST_STR_LEN("<tr><td class=\"n\"><a href=\""));
1215 		buffer_append_string_encoded(out, DIRLIST_ENT_NAME(tmp), tmp->namelen, ENCODING_REL_URI_PART);
1216 		buffer_append_string_len(out, CONST_STR_LEN("\">"));
1217 		buffer_append_string_encoded(out, DIRLIST_ENT_NAME(tmp), tmp->namelen, ENCODING_MINIMAL_XML);
1218 		buffer_append_string_len(out, CONST_STR_LEN("</a></td><td class=\"m\">"));
1219 		buffer_append_strftime(out, "%Y-%b-%d %T", localtime64_r(&tmp->mtime, &tm));
1220 		size_t buflen =
1221 		  http_list_directory_sizefmt(sizebuf, sizeof(sizebuf), tmp->size);
1222 		struct const_iovec iov[] = {
1223 		  { CONST_STR_LEN("</td><td class=\"s\">") }
1224 		 ,{ sizebuf, buflen }
1225 		 ,{ CONST_STR_LEN("</td><td class=\"t\">") }
1226 		 ,{ BUF_PTR_LEN(content_type) }
1227 		 ,{ CONST_STR_LEN("</td></tr>\n") }
1228 		};
1229 		buffer_append_iovec(out, iov, sizeof(iov)/sizeof(*iov));
1230 
1231 		if (buffer_string_space(out) < 256) {
1232 			if (out == tb) {
1233 				if (0 != chunkqueue_append_mem_to_tempfile(cq,
1234 				                                           BUF_PTR_LEN(out),
1235 				                                           r->conf.errh))
1236 					break;
1237 				buffer_clear(out);
1238 			}
1239 		}
1240 	}
1241 
1242 	if (out == tb) {
1243 		if (!buffer_is_blank(out)
1244 		    && 0 != chunkqueue_append_mem_to_tempfile(cq,
1245 		                                              BUF_PTR_LEN(out),
1246 		                                              r->conf.errh)) {
1247 			/* ignore */
1248 		}
1249 	}
1250 	else {
1251 		chunkqueue_append_buffer_commit(cq);
1252 	}
1253 }
1254 
1255 
1256 static void mod_dirlisting_content_type (request_st * const r, const buffer * const encoding) {
1257     buffer * const vb =
1258       http_header_response_set_ptr(r, HTTP_HEADER_CONTENT_TYPE,
1259                                    CONST_STR_LEN("Content-Type"));
1260     if (NULL == encoding)
1261         buffer_copy_string_len(vb, CONST_STR_LEN("text/html"));
1262     else
1263         buffer_append_str2(vb, CONST_STR_LEN("text/html;charset="),
1264                                BUF_PTR_LEN(encoding));
1265 }
1266 
1267 
1268 static void mod_dirlisting_response (request_st * const r, handler_ctx * const hctx) {
1269     http_list_directory_header(r, hctx);
1270     http_list_directory(r, hctx);
1271     http_list_directory_footer(r, hctx);
1272     mod_dirlisting_content_type(r, hctx->conf.encoding);
1273 
1274     r->resp_body_finished = 1;
1275 }
1276 
1277 
1278 static void mod_dirlisting_json_append (request_st * const r, handler_ctx * const hctx, const int fin) {
1279     buffer * const jb = hctx->jb;
1280     if (fin)
1281         buffer_append_string_len(jb, CONST_STR_LEN("]}"));
1282     else if (buffer_clen(jb) < 16384-1024)
1283         return; /* aggregate bunches of entries, even if streaming response */
1284 
1285     if (hctx->jfn) {
1286         if (__builtin_expect( (write_all(hctx->jfd, BUF_PTR_LEN(jb)) < 0), 0)) {
1287             /*(cleanup, cease caching if error occurs writing to cache file)*/
1288             unlink(hctx->jfn);
1289             free(hctx->jfn);
1290             hctx->jfn = NULL;
1291             close(hctx->jfd);
1292             hctx->jfd = -1;
1293         }
1294         /* Note: writing cache file is separate from the response so that if an
1295          * error occurs with cache, the response still proceeds.  While this is
1296          * duplicative if the response is large enough to spill to temporary
1297          * files, it is expected that only very large directories will spill to
1298          * temporary files, and even then most responses will be less than 1 MB.
1299          * The cache path can be different from server.upload-dirs.
1300          * Note: since responses are not expected to be large, no effort is
1301          * currently made here to handle FDEVENT_STREAM_RESPONSE_BUFMIN and to
1302          * defer reading more from directory while data is sent to client */
1303     }
1304 
1305     http_chunk_append_buffer(r, jb); /* clears jb */
1306 }
1307 
1308 
1309 SUBREQUEST_FUNC(mod_dirlisting_subrequest);
1310 REQUEST_FUNC(mod_dirlisting_reset);
1311 static handler_t mod_dirlisting_cache_check (request_st * const r, plugin_data * const p);
1312 __attribute_noinline__
1313 static void mod_dirlisting_cache_add (request_st * const r, handler_ctx * const hctx);
1314 __attribute_noinline__
1315 static void mod_dirlisting_cache_json_init (request_st * const r, handler_ctx * const hctx);
1316 __attribute_noinline__
1317 static void mod_dirlisting_cache_json (request_st * const r, handler_ctx * const hctx);
1318 
1319 
1320 URIHANDLER_FUNC(mod_dirlisting_subrequest_start) {
1321 	plugin_data *p = p_d;
1322 
1323 	if (NULL != r->handler_module) return HANDLER_GO_ON;
1324 	if (!buffer_has_slash_suffix(&r->uri.path)) return HANDLER_GO_ON;
1325 	if (!http_method_get_or_head(r->http_method)) return HANDLER_GO_ON;
1326 	/* r->physical.path is non-empty for handle_subrequest_start */
1327 	/*if (buffer_is_blank(&r->physical.path)) return HANDLER_GO_ON;*/
1328 
1329 	mod_dirlisting_patch_config(r, p);
1330 
1331 	if (!p->conf.dir_listing) return HANDLER_GO_ON;
1332 
1333 	if (r->conf.log_request_handling) {
1334 		log_error(r->conf.errh, __FILE__, __LINE__,
1335 		  "-- handling the request as Dir-Listing");
1336 		log_error(r->conf.errh, __FILE__, __LINE__,
1337 		  "URI          : %s", r->uri.path.ptr);
1338 	}
1339 
1340   #if 0 /* redundant check; not necessary */
1341 	/* r->physical.path is a dir since it ends in slash, or else
1342 	 * http_response_physical_path_check() would have redirected
1343 	 * before calling handle_subrequest_start */
1344 	if (!stat_cache_path_isdir(&r->physical.path)) {
1345 		if (errno == ENOTDIR)
1346 			return HANDLER_GO_ON;
1347 		log_perror(r->conf.errh,__FILE__,__LINE__,"%s",r->physical.path.ptr);
1348 		r->http_status = 500;
1349 		return HANDLER_FINISHED;
1350 	}
1351   #endif
1352 
1353 	/* TODO: add option/mechanism to enable json output */
1354   #if 0 /* XXX: ??? might this be enabled accidentally by clients ??? */
1355 	const buffer * const vb =
1356 	  http_header_request_get(r, HTTP_HEADER_ACCEPT, CONST_STR_LEN("Accept"));
1357 	p->conf.json = (vb && strstr(vb->ptr, "application/json")); /*(coarse)*/
1358   #endif
1359 
1360 	if (p->conf.cache) {
1361 		handler_t rc = mod_dirlisting_cache_check(r, p);
1362 		if (rc != HANDLER_GO_ON)
1363 			return rc;
1364 	}
1365 
1366 	/* upper limit for dirlisting requests in progress (per lighttpd worker)
1367 	 * (attempt to avoid "livelock" scenarios or starvation of other requests)
1368 	 * (100 is still a high arbitrary limit;
1369 	 *  and limit applies only to directories larger than DIRLIST_BATCH-2) */
1370 	if (p->processing == dirlist_max_in_progress) {
1371 		r->http_status = 503;
1372 		http_header_response_set(r, HTTP_HEADER_OTHER,
1373 		                         CONST_STR_LEN("Retry-After"),
1374 		                         CONST_STR_LEN("2"));
1375 		return HANDLER_FINISHED;
1376 	}
1377 
1378 	handler_ctx * const hctx = mod_dirlisting_handler_ctx_init(p);
1379 
1380 	/* future: might implement a queue to limit max number of dirlisting
1381 	 * requests being serviced in parallel (increasing disk I/O), and if
1382 	 * caching is enabled, to avoid repeating the work on the same directory
1383 	 * in parallel.  Could continue serving (expired) cached entry while
1384 	 * updating, but burst of requests on first access to dir would still
1385 	 * need to be handled.
1386 	 *
1387 	 * If queueing (not implemented), defer opening dir until pulled off
1388 	 * queue.  Since joblist is per-connection, would need to handle single
1389 	 * request from queue even if multiple streams are queued on same
1390 	 * HTTP/2 connection.  If queueing, must check for and remove from
1391 	 * queue in mod_dirlisting_reset() if request is still queued. */
1392 
1393 	if (0 != http_open_directory(r, hctx)) {
1394 		/* dirlisting failed */
1395 		r->http_status = 403;
1396 	        mod_dirlisting_handler_ctx_free(hctx);
1397 		return HANDLER_FINISHED;
1398 	}
1399 	++p->processing;
1400 
1401 	if (p->conf.json) {
1402 		hctx->jfd = -1;
1403 		hctx->jb = chunk_buffer_acquire();
1404 		buffer_append_string_len(hctx->jb, CONST_STR_LEN("{["));
1405 		if (p->conf.cache)
1406 			mod_dirlisting_cache_json_init(r, hctx);
1407 		http_header_response_set(r, HTTP_HEADER_CONTENT_TYPE,
1408 		                         CONST_STR_LEN("Content-Type"),
1409 		                         CONST_STR_LEN("application/json"));
1410 		r->http_status = 200;
1411 		r->resp_body_started = 1;
1412 	}
1413 
1414 	r->plugin_ctx[p->id] = hctx;
1415 	r->handler_module = p->self;
1416 	return mod_dirlisting_subrequest(r, p);
1417 }
1418 
1419 
1420 SUBREQUEST_FUNC(mod_dirlisting_subrequest) {
1421     plugin_data * const p = p_d;
1422     handler_ctx * const hctx = r->plugin_ctx[p->id];
1423     if (NULL == hctx) return HANDLER_GO_ON; /*(should not happen)*/
1424 
1425     handler_t rc = http_read_directory(hctx);
1426     switch (rc) {
1427       case HANDLER_FINISHED:
1428         if (hctx->jb) { /* (hctx->conf.json) */
1429             mod_dirlisting_json_append(r, hctx, 1);
1430             r->resp_body_finished = 1;
1431             if (hctx->jfn) /* (also (hctx->conf.cache) */
1432                 mod_dirlisting_cache_json(r, hctx);
1433         }
1434         else {
1435             mod_dirlisting_response(r, hctx);
1436             if (hctx->conf.cache)
1437                 mod_dirlisting_cache_add(r, hctx);
1438         }
1439         mod_dirlisting_reset(r, p); /*(release resources, including hctx)*/
1440         break;
1441       case HANDLER_WAIT_FOR_EVENT: /*(used here to mean 'yield')*/
1442         if (hctx->jb)   /* (hctx->conf.json) */
1443             mod_dirlisting_json_append(r, hctx, 0);
1444         joblist_append(r->con);
1445         break;
1446       default:
1447         break;
1448     }
1449 
1450     return rc;
1451 }
1452 
1453 
1454 REQUEST_FUNC(mod_dirlisting_reset) {
1455     void ** const restrict dptr = &r->plugin_ctx[((plugin_data *)p_d)->id];
1456     if (*dptr) {
1457         --((plugin_data *)p_d)->processing;
1458         mod_dirlisting_handler_ctx_free(*dptr);
1459         *dptr = NULL;
1460     }
1461     return HANDLER_GO_ON;
1462 }
1463 
1464 
1465 static handler_t mod_dirlisting_cache_check (request_st * const r, plugin_data * const p) {
1466     /* optional: an external process can trigger a refresh by deleting the cache
1467      * entry when the external process detects (or initiaties) changes to dir */
1468     buffer * const tb = r->tmp_buf;
1469     buffer_copy_path_len2(tb, BUF_PTR_LEN(p->conf.cache->path),
1470                               BUF_PTR_LEN(&r->physical.path));
1471     buffer_append_string_len(tb, p->conf.json ? "dirlist.json" : "dirlist.html",
1472                              sizeof("dirlist.html")-1);
1473     stat_cache_entry * const sce = stat_cache_get_entry_open(tb, 1);
1474     if (NULL == sce || sce->fd == -1)
1475         return HANDLER_GO_ON;
1476     if (TIME64_CAST(sce->st.st_mtime) + p->conf.cache->max_age < log_epoch_secs)
1477         return HANDLER_GO_ON;
1478 
1479     !p->conf.json
1480       ? mod_dirlisting_content_type(r, p->conf.encoding)
1481       : http_header_response_set(r, HTTP_HEADER_CONTENT_TYPE,
1482                                  CONST_STR_LEN("Content-Type"),
1483                                  CONST_STR_LEN("application/json"));
1484 
1485   #if 0
1486     /*(XXX: ETag needs to be set for mod_deflate to potentially handle)*/
1487     /*(XXX: should ETag be created from cache file mtime or directory mtime?)*/
1488     const int follow_symlink = r->conf.follow_symlink;
1489     r->conf.follow_symlink = 1; /*(skip symlink checks into cache)*/
1490     http_response_send_file(r, sce->name, sce);
1491     r->conf.follow_symlink = follow_symlink;
1492     if (r->http_status < 400)
1493         return HANDLER_FINISHED;
1494     r->http_status = 0;
1495   #endif
1496 
1497     /* Note: dirlist < 350 or so entries will generally trigger file
1498      * read into memory for dirlist < 32k, which will not be able to use
1499      * mod_deflate cache.  Still, this is much more efficient than lots of
1500      * stat() calls to generate the dirlisting for each and every request */
1501     if (0 != http_chunk_append_file_ref(r, sce)) {
1502         http_header_response_unset(r, HTTP_HEADER_CONTENT_TYPE,
1503                                    CONST_STR_LEN("Content-Type"));
1504         http_response_body_clear(r, 0);
1505         return HANDLER_GO_ON;
1506     }
1507 
1508     r->resp_body_finished = 1;
1509     return HANDLER_FINISHED;
1510 }
1511 
1512 
1513 static int mod_dirlisting_write_cq (const int fd, chunkqueue * const cq, log_error_st * const errh)
1514 {
1515     chunkqueue in;
1516     memset(&in, 0, sizeof(in));
1517     chunkqueue_append_chunkqueue(&in, cq);
1518     cq->bytes_in  -= in.bytes_in;
1519     cq->bytes_out -= in.bytes_in;
1520 
1521     /*(similar to mod_webdav.c:mod_webdav_write_cq(), but operates on two cqs)*/
1522     while (!chunkqueue_is_empty(&in)) {
1523         ssize_t wr = chunkqueue_write_chunk(fd, &in, errh);
1524         if (__builtin_expect( (wr > 0), 1))
1525             chunkqueue_steal(cq, &in, wr);
1526         else if (wr < 0) {
1527             /*(writing to tempfile failed; transfer remaining data back to cq)*/
1528             chunkqueue_append_chunkqueue(cq, &in);
1529             return 0;
1530         }
1531         else /*(wr == 0)*/
1532             chunkqueue_remove_finished_chunks(&in);
1533     }
1534     return 1;
1535 }
1536 
1537 
1538 #include <sys/stat.h>   /* mkdir() */
1539 #include <sys/types.h>
1540 /*(similar to mod_deflate.c:mkdir_recursive(), but starts mid-path)*/
1541 static int mkdir_recursive (char *dir, size_t off) {
1542     char *p = dir+off;
1543     if (*p != '/') {
1544         if (off && p[-1] == '/')
1545             --p;
1546         else {
1547             errno = ENOTDIR;
1548             return -1;
1549         }
1550     }
1551     do {
1552         *p = '\0';
1553         int rc = mkdir(dir, 0700);
1554         *p = '/';
1555         if (0 != rc && errno != EEXIST) return -1;
1556     } while ((p = strchr(p+1, '/')) != NULL);
1557     return 0;
1558 }
1559 
1560 
1561 #include <stdio.h>      /* rename() */
1562 __attribute_noinline__
1563 static void mod_dirlisting_cache_add (request_st * const r, handler_ctx * const hctx) {
1564   #ifndef PATH_MAX
1565   #define PATH_MAX 4096
1566   #endif
1567     char oldpath[PATH_MAX];
1568     char newpath[PATH_MAX];
1569     buffer * const tb = r->tmp_buf;
1570     buffer_copy_path_len2(tb, BUF_PTR_LEN(hctx->conf.cache->path),
1571                               BUF_PTR_LEN(&r->physical.path));
1572     if (!stat_cache_path_isdir(tb)
1573         && 0 != mkdir_recursive(tb->ptr, buffer_clen(hctx->conf.cache->path)))
1574         return;
1575     buffer_append_string_len(tb, CONST_STR_LEN("dirlist.html"));
1576     const size_t len = buffer_clen(tb);
1577     if (len + 7 >= PATH_MAX) return;
1578     memcpy(newpath, tb->ptr, len+1);   /*(include '\0')*/
1579     buffer_append_string_len(tb, CONST_STR_LEN(".XXXXXX"));
1580     memcpy(oldpath, tb->ptr, len+7+1); /*(include '\0')*/
1581     const int fd = fdevent_mkostemp(oldpath, 0);
1582     if (fd < 0) return;
1583     if (mod_dirlisting_write_cq(fd, &r->write_queue, r->conf.errh)
1584         && 0 == rename(oldpath, newpath))
1585         stat_cache_invalidate_entry(newpath, len);
1586     else
1587         unlink(oldpath);
1588     close(fd);
1589 }
1590 
1591 
1592 __attribute_noinline__
1593 static void mod_dirlisting_cache_json_init (request_st * const r, handler_ctx * const hctx) {
1594   #ifndef PATH_MAX
1595   #define PATH_MAX 4096
1596   #endif
1597     buffer * const tb = r->tmp_buf;
1598     buffer_copy_path_len2(tb, BUF_PTR_LEN(hctx->conf.cache->path),
1599                               BUF_PTR_LEN(&r->physical.path));
1600     if (!stat_cache_path_isdir(tb)
1601         && 0 != mkdir_recursive(tb->ptr, buffer_clen(hctx->conf.cache->path)))
1602         return;
1603     buffer_append_string_len(tb, CONST_STR_LEN("dirlist.json.XXXXXX"));
1604     const int fd = fdevent_mkostemp(tb->ptr, 0);
1605     if (fd < 0) return;
1606     hctx->jfn_len = buffer_clen(tb);
1607     hctx->jfd = fd;
1608     hctx->jfn = malloc(hctx->jfn_len+1);
1609     force_assert(hctx->jfn);
1610     memcpy(hctx->jfn, tb->ptr, hctx->jfn_len+1); /*(include '\0')*/
1611 }
1612 
1613 
1614 __attribute_noinline__
1615 static void mod_dirlisting_cache_json (request_st * const r, handler_ctx * const hctx) {
1616   #ifndef PATH_MAX
1617   #define PATH_MAX 4096
1618   #endif
1619     UNUSED(r);
1620     char newpath[PATH_MAX];
1621     const size_t len = hctx->jfn_len - 7; /*(-7 for .XXXXXX)*/
1622     force_assert(len < PATH_MAX);
1623     memcpy(newpath, hctx->jfn, len);
1624     newpath[len] = '\0';
1625     if (0 == rename(hctx->jfn, newpath))
1626         stat_cache_invalidate_entry(newpath, len);
1627     else
1628         unlink(hctx->jfn);
1629     free(hctx->jfn);
1630     hctx->jfn = NULL;
1631 }
1632 
1633 
1634 int mod_dirlisting_plugin_init(plugin *p);
1635 int mod_dirlisting_plugin_init(plugin *p) {
1636 	p->version     = LIGHTTPD_VERSION_ID;
1637 	p->name        = "dirlisting";
1638 
1639 	p->init        = mod_dirlisting_init;
1640 	p->handle_subrequest_start = mod_dirlisting_subrequest_start;
1641 	p->handle_subrequest       = mod_dirlisting_subrequest;
1642 	p->handle_request_reset    = mod_dirlisting_reset;
1643 	p->set_defaults  = mod_dirlisting_set_defaults;
1644 	p->cleanup     = mod_dirlisting_free;
1645 
1646 	return 0;
1647 }
1648