xref: /lighttpd1.4/src/mod_dirlisting.c (revision f2e4e83a)
1 #include "first.h"
2 
3 #include "base.h"
4 #include "log.h"
5 #include "buffer.h"
6 #include "fdevent.h"
7 #include "http_header.h"
8 
9 #include "plugin.h"
10 
11 #include "stat_cache.h"
12 
13 #include <stdlib.h>
14 #include <string.h>
15 #include <dirent.h>
16 #include <fcntl.h>
17 #include <unistd.h>
18 #include <time.h>
19 
20 #ifdef HAVE_PCRE_H
21 #include <pcre.h>
22 #endif
23 
24 /**
25  * this is a dirlisting for a lighttpd plugin
26  */
27 
28 typedef struct {
29 	char dir_listing;
30 	char hide_dot_files;
31 	char hide_readme_file;
32 	char encode_readme;
33 	char hide_header_file;
34 	char encode_header;
35 	char auto_layout;
36 
37       #ifdef HAVE_PCRE_H
38 	pcre **excludes;
39       #else
40 	void *excludes;
41       #endif
42 
43 	const buffer *show_readme;
44 	const buffer *show_header;
45 	const buffer *external_css;
46 	const buffer *external_js;
47 	const buffer *encoding;
48 	const buffer *set_footer;
49 } plugin_config;
50 
51 typedef struct {
52 	PLUGIN_DATA;
53 	plugin_config defaults;
54 	plugin_config conf;
55 
56 	buffer tmp_buf;
57 } plugin_data;
58 
59 #ifdef HAVE_PCRE_H
60 
61 static pcre ** mod_dirlisting_parse_excludes(server *srv, const array *a) {
62     pcre **regexes = calloc(a->used + 1, sizeof(pcre *));
63     force_assert(regexes);
64     for (uint32_t j = 0; j < a->used; ++j) {
65         const data_string *ds = (const data_string *)a->data[j];
66         const char *errptr;
67         int erroff;
68         regexes[j] = pcre_compile(ds->value.ptr, 0, &errptr, &erroff, NULL);
69         if (NULL == regexes[j]) {
70             log_error(srv->errh, __FILE__, __LINE__,
71               "pcre_compile failed for: %s", ds->value.ptr);
72             for (pcre **regex = regexes; *regex; ++regex) pcre_free(*regex);
73             free(regexes);
74             return NULL;
75         }
76     }
77     return regexes;
78 }
79 
80 static int mod_dirlisting_exclude(log_error_st *errh, pcre **regex, const char *name, size_t len) {
81     for(; *regex; ++regex) {
82         #define N 10
83         int ovec[N * 3];
84         int n;
85         if ((n = pcre_exec(*regex, NULL, name, len, 0, 0, ovec, 3 * N)) < 0) {
86             if (n == PCRE_ERROR_NOMATCH) continue;
87 
88             log_error(errh, __FILE__, __LINE__,
89               "execution error while matching: %d", n);
90             /* aborting would require a lot of manual cleanup here.
91              * skip instead (to not leak names that break pcre matching)
92              */
93         }
94         return 1;
95         #undef N
96     }
97     return 0; /* no match */
98 }
99 
100 #else
101 
102 #define mod_dirlisting_exclude(a, b, c, d) 0
103 
104 #endif
105 
106 
107 INIT_FUNC(mod_dirlisting_init) {
108     return calloc(1, sizeof(plugin_data));
109 }
110 
111 FREE_FUNC(mod_dirlisting_free) {
112     plugin_data * const p = p_d;
113     free(p->tmp_buf.ptr);
114     if (NULL == p->cvlist) return;
115     /* (init i to 0 if global context; to 1 to skip empty global context) */
116     for (int i = !p->cvlist[0].v.u2[1], used = p->nconfig; i < used; ++i) {
117         config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
118         for (; -1 != cpv->k_id; ++cpv) {
119             switch (cpv->k_id) {
120              #ifdef HAVE_PCRE_H
121               case 2: /* dir-listing.exclude */
122                 if (cpv->vtype != T_CONFIG_LOCAL) continue;
123                 for (pcre **regex = cpv->v.v; *regex; ++regex)
124                     pcre_free(*regex);
125                 free(cpv->v.v);
126                 break;
127              #endif
128               default:
129                 break;
130             }
131         }
132     }
133 }
134 
135 static void mod_dirlisting_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) {
136     switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */
137       case 0: /* dir-listing.activate */
138       case 1: /* server.dir-listing *//*(historical)*/
139         pconf->dir_listing = (char)cpv->v.u;
140         break;
141       case 2: /* dir-listing.exclude */
142         if (cpv->vtype == T_CONFIG_LOCAL)
143             pconf->excludes = cpv->v.v;
144         break;
145       case 3: /* dir-listing.hide-dotfiles */
146         pconf->hide_dot_files = (char)cpv->v.u;
147         break;
148       case 4: /* dir-listing.external-css */
149         pconf->external_css = cpv->v.b;
150         break;
151       case 5: /* dir-listing.external-js */
152         pconf->external_js = cpv->v.b;
153         break;
154       case 6: /* dir-listing.encoding */
155         pconf->encoding = cpv->v.b;
156         break;
157       case 7: /* dir-listing.show-readme */
158         pconf->show_readme = cpv->v.b;
159         break;
160       case 8: /* dir-listing.hide-readme-file */
161         pconf->hide_readme_file = (char)cpv->v.u;
162         break;
163       case 9: /* dir-listing.show-header */
164         pconf->show_header = cpv->v.b;
165         break;
166       case 10:/* dir-listing.hide-header-file */
167         pconf->hide_header_file = (char)cpv->v.u;
168         break;
169       case 11:/* dir-listing.set-footer */
170         pconf->set_footer = cpv->v.b;
171         break;
172       case 12:/* dir-listing.encode-readme */
173         pconf->encode_readme = (char)cpv->v.u;
174         break;
175       case 13:/* dir-listing.encode-header */
176         pconf->encode_header = (char)cpv->v.u;
177         break;
178       case 14:/* dir-listing.auto-layout */
179         pconf->auto_layout = (char)cpv->v.u;
180         break;
181       default:/* should not happen */
182         return;
183     }
184 }
185 
186 static void mod_dirlisting_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) {
187     do {
188         mod_dirlisting_merge_config_cpv(pconf, cpv);
189     } while ((++cpv)->k_id != -1);
190 }
191 
192 static void mod_dirlisting_patch_config(request_st * const r, plugin_data * const p) {
193     memcpy(&p->conf, &p->defaults, sizeof(plugin_config));
194     for (int i = 1, used = p->nconfig; i < used; ++i) {
195         if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id))
196             mod_dirlisting_merge_config(&p->conf, p->cvlist + p->cvlist[i].v.u2[0]);
197     }
198 }
199 
200 SETDEFAULTS_FUNC(mod_dirlisting_set_defaults) {
201     static const config_plugin_keys_t cpk[] = {
202       { CONST_STR_LEN("dir-listing.activate"),
203         T_CONFIG_BOOL,
204         T_CONFIG_SCOPE_CONNECTION }
205      ,{ CONST_STR_LEN("server.dir-listing"),
206         T_CONFIG_BOOL,
207         T_CONFIG_SCOPE_CONNECTION }
208      ,{ CONST_STR_LEN("dir-listing.exclude"),
209         T_CONFIG_ARRAY_VLIST,
210         T_CONFIG_SCOPE_CONNECTION }
211      ,{ CONST_STR_LEN("dir-listing.hide-dotfiles"),
212         T_CONFIG_BOOL,
213         T_CONFIG_SCOPE_CONNECTION }
214      ,{ CONST_STR_LEN("dir-listing.external-css"),
215         T_CONFIG_STRING,
216         T_CONFIG_SCOPE_CONNECTION }
217      ,{ CONST_STR_LEN("dir-listing.external-js"),
218         T_CONFIG_STRING,
219         T_CONFIG_SCOPE_CONNECTION }
220      ,{ CONST_STR_LEN("dir-listing.encoding"),
221         T_CONFIG_STRING,
222         T_CONFIG_SCOPE_CONNECTION }
223      ,{ CONST_STR_LEN("dir-listing.show-readme"),
224         T_CONFIG_STRING,
225         T_CONFIG_SCOPE_CONNECTION }
226      ,{ CONST_STR_LEN("dir-listing.hide-readme-file"),
227         T_CONFIG_BOOL,
228         T_CONFIG_SCOPE_CONNECTION }
229      ,{ CONST_STR_LEN("dir-listing.show-header"),
230         T_CONFIG_STRING,
231         T_CONFIG_SCOPE_CONNECTION }
232      ,{ CONST_STR_LEN("dir-listing.hide-header-file"),
233         T_CONFIG_BOOL,
234         T_CONFIG_SCOPE_CONNECTION }
235      ,{ CONST_STR_LEN("dir-listing.set-footer"),
236         T_CONFIG_STRING,
237         T_CONFIG_SCOPE_CONNECTION }
238      ,{ CONST_STR_LEN("dir-listing.encode-readme"),
239         T_CONFIG_BOOL,
240         T_CONFIG_SCOPE_CONNECTION }
241      ,{ CONST_STR_LEN("dir-listing.encode-header"),
242         T_CONFIG_BOOL,
243         T_CONFIG_SCOPE_CONNECTION }
244      ,{ CONST_STR_LEN("dir-listing.auto-layout"),
245         T_CONFIG_BOOL,
246         T_CONFIG_SCOPE_CONNECTION }
247      ,{ NULL, 0,
248         T_CONFIG_UNSET,
249         T_CONFIG_SCOPE_UNSET }
250     };
251 
252     plugin_data * const p = p_d;
253     if (!config_plugin_values_init(srv, p, cpk, "mod_dirlisting"))
254         return HANDLER_ERROR;
255 
256     /* process and validate config directives
257      * (init i to 0 if global context; to 1 to skip empty global context) */
258     for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) {
259         config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
260         for (; -1 != cpv->k_id; ++cpv) {
261             switch (cpv->k_id) {
262               case 0: /* dir-listing.activate */
263               case 1: /* server.dir-listing *//*(historical)*/
264                 break;
265               case 2: /* dir-listing.exclude */
266                #ifndef HAVE_PCRE_H
267                 if (cpv->v.a->used > 0) {
268                     log_error(srv->errh, __FILE__, __LINE__,
269                       "pcre support is missing for: %s, "
270                       "please install libpcre and the headers",
271                       cpk[cpv->k_id].k);
272                     return HANDLER_ERROR;
273                 }
274                #else
275                 cpv->v.v = mod_dirlisting_parse_excludes(srv, cpv->v.a);
276                 if (NULL == cpv->v.v) {
277                     log_error(srv->errh, __FILE__, __LINE__,
278                       "unexpected value for %s", cpk[cpv->k_id].k);
279                     return HANDLER_ERROR;
280                 }
281                 cpv->vtype = T_CONFIG_LOCAL;
282                #endif
283                 break;
284               case 3: /* dir-listing.hide-dotfiles */
285               case 4: /* dir-listing.external-css */
286               case 5: /* dir-listing.external-js */
287               case 6: /* dir-listing.encoding */
288                 break;
289               case 7: /* dir-listing.show-readme */
290                 if (!buffer_string_is_empty(cpv->v.b)) {
291                     buffer *b;
292                     *(const buffer **)&b = cpv->v.b;
293                     if (buffer_is_equal_string(b, CONST_STR_LEN("enable")))
294                         buffer_copy_string_len(b, CONST_STR_LEN("README.txt"));
295                     else if (buffer_is_equal_string(b,CONST_STR_LEN("disable")))
296                         buffer_clear(b);
297                 }
298                 break;
299               case 8: /* dir-listing.hide-readme-file */
300                 break;
301               case 9: /* dir-listing.show-header */
302                 if (!buffer_string_is_empty(cpv->v.b)) {
303                     buffer *b;
304                     *(const buffer **)&b = cpv->v.b;
305                     if (buffer_is_equal_string(b, CONST_STR_LEN("enable")))
306                         buffer_copy_string_len(b, CONST_STR_LEN("HEADER.txt"));
307                     else if (buffer_is_equal_string(b,CONST_STR_LEN("disable")))
308                         buffer_clear(b);
309                 }
310                 break;
311               case 10:/* dir-listing.hide-header-file */
312               case 11:/* dir-listing.set-footer */
313               case 12:/* dir-listing.encode-readme */
314               case 13:/* dir-listing.encode-header */
315               case 14:/* dir-listing.auto-layout */
316                 break;
317               default:/* should not happen */
318                 break;
319             }
320         }
321     }
322 
323     p->defaults.dir_listing = 0;
324     p->defaults.hide_dot_files = 1;
325     p->defaults.hide_readme_file = 0;
326     p->defaults.hide_header_file = 0;
327     p->defaults.encode_readme = 1;
328     p->defaults.encode_header = 1;
329     p->defaults.auto_layout = 1;
330 
331     /* initialize p->defaults from global config context */
332     if (p->nconfig > 0 && p->cvlist->v.u2[1]) {
333         const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0];
334         if (-1 != cpv->k_id)
335             mod_dirlisting_merge_config(&p->defaults, cpv);
336     }
337 
338     return HANDLER_GO_ON;
339 }
340 
341 typedef struct {
342 	uint32_t namelen;
343 	time_t  mtime;
344 	off_t   size;
345 } dirls_entry_t;
346 
347 typedef struct {
348 	dirls_entry_t **ent;
349 	uint32_t used;
350 	uint32_t size;
351 } dirls_list_t;
352 
353 #define DIRLIST_ENT_NAME(ent)	((char*)(ent) + sizeof(dirls_entry_t))
354 #define DIRLIST_BLOB_SIZE		16
355 
356 /* simple combsort algorithm */
357 static void http_dirls_sort(dirls_entry_t **ent, int num) {
358 	int gap = num;
359 	int i, j;
360 	int swapped;
361 	dirls_entry_t *tmp;
362 
363 	do {
364 		gap = (gap * 10) / 13;
365 		if (gap == 9 || gap == 10)
366 			gap = 11;
367 		if (gap < 1)
368 			gap = 1;
369 		swapped = 0;
370 
371 		for (i = 0; i < num - gap; i++) {
372 			j = i + gap;
373 			if (strcmp(DIRLIST_ENT_NAME(ent[i]), DIRLIST_ENT_NAME(ent[j])) > 0) {
374 				tmp = ent[i];
375 				ent[i] = ent[j];
376 				ent[j] = tmp;
377 				swapped = 1;
378 			}
379 		}
380 
381 	} while (gap > 1 || swapped);
382 }
383 
384 /* buffer must be able to hold "999.9K"
385  * conversion is simple but not perfect
386  */
387 static int http_list_directory_sizefmt(char *buf, size_t bufsz, off_t size) {
388 	const char unit[] = " KMGTPE";	/* Kilo, Mega, Giga, Tera, Peta, Exa */
389 	const char *u = unit;		/* u will always increment at least once */
390 	int remain;
391 	size_t buflen;
392 
393 	if (size < 100)
394 		size += 99;
395 	if (size < 100)
396 		size = 0;
397 
398 	while (1) {
399 		remain = (int) size & 1023;
400 		size >>= 10;
401 		u++;
402 		if ((size & (~0 ^ 1023)) == 0)
403 			break;
404 	}
405 
406 	remain /= 100;
407 	if (remain > 9)
408 		remain = 9;
409 	if (size > 999) {
410 		size   = 0;
411 		remain = 9;
412 		u++;
413 	}
414 
415 	buflen = li_itostrn(buf, bufsz, size);
416 	if (buflen + 3 >= bufsz) return buflen;
417 	buf[buflen+0] = '.';
418 	buf[buflen+1] = remain + '0';
419 	buf[buflen+2] = *u;
420 	buf[buflen+3] = '\0';
421 
422 	return buflen + 3;
423 }
424 
425 static void http_list_directory_include_file(buffer *out, int symlinks, const buffer *path, const char *classname, int encode) {
426 	int fd = fdevent_open_cloexec(path->ptr, symlinks, O_RDONLY, 0);
427 	ssize_t rd;
428 	char buf[8192];
429 
430 	if (-1 == fd) return;
431 
432 	if (encode) {
433 		buffer_append_string_len(out, CONST_STR_LEN("<pre class=\""));
434 		buffer_append_string(out, classname);
435 		buffer_append_string_len(out, CONST_STR_LEN("\">"));
436 	}
437 
438 	while ((rd = read(fd, buf, sizeof(buf))) > 0) {
439 		if (encode) {
440 			buffer_append_string_encoded(out, buf, (size_t)rd, ENCODING_MINIMAL_XML);
441 		} else {
442 			buffer_append_string_len(out, buf, (size_t)rd);
443 		}
444 	}
445 	close(fd);
446 
447 	if (encode) {
448 		buffer_append_string_len(out, CONST_STR_LEN("</pre>"));
449 	}
450 }
451 
452 /* portions copied from mod_status
453  * modified and specialized for stable dirlist sorting by name */
454 static const char js_simple_table_resort[] = \
455 "var click_column;\n" \
456 "var name_column = 0;\n" \
457 "var date_column = 1;\n" \
458 "var size_column = 2;\n" \
459 "var type_column = 3;\n" \
460 "var prev_span = null;\n" \
461 "\n" \
462 "if (typeof(String.prototype.localeCompare) === 'undefined') {\n" \
463 " String.prototype.localeCompare = function(str, locale, options) {\n" \
464 "   return ((this == str) ? 0 : ((this > str) ? 1 : -1));\n" \
465 " };\n" \
466 "}\n" \
467 "\n" \
468 "if (typeof(String.prototype.toLocaleUpperCase) === 'undefined') {\n" \
469 " String.prototype.toLocaleUpperCase = function() {\n" \
470 "  return this.toUpperCase();\n" \
471 " };\n" \
472 "}\n" \
473 "\n" \
474 "function get_inner_text(el) {\n" \
475 " if((typeof el == 'string')||(typeof el == 'undefined'))\n" \
476 "  return el;\n" \
477 " if(el.innerText)\n" \
478 "  return el.innerText;\n" \
479 " else {\n" \
480 "  var str = \"\";\n" \
481 "  var cs = el.childNodes;\n" \
482 "  var l = cs.length;\n" \
483 "  for (i=0;i<l;i++) {\n" \
484 "   if (cs[i].nodeType==1) str += get_inner_text(cs[i]);\n" \
485 "   else if (cs[i].nodeType==3) str += cs[i].nodeValue;\n" \
486 "  }\n" \
487 " }\n" \
488 " return str;\n" \
489 "}\n" \
490 "\n" \
491 "function isdigit(c) {\n" \
492 " return (c >= '0' && c <= '9');\n" \
493 "}\n" \
494 "\n" \
495 "function unit_multiplier(unit) {\n" \
496 " return (unit=='K') ? 1000\n" \
497 "      : (unit=='M') ? 1000000\n" \
498 "      : (unit=='G') ? 1000000000\n" \
499 "      : (unit=='T') ? 1000000000000\n" \
500 "      : (unit=='P') ? 1000000000000000\n" \
501 "      : (unit=='E') ? 1000000000000000000 : 1;\n" \
502 "}\n" \
503 "\n" \
504 "var li_date_regex=/(\\d{4})-(\\w{3})-(\\d{2}) (\\d{2}):(\\d{2}):(\\d{2})/;\n" \
505 "\n" \
506 "var li_mon = ['Jan','Feb','Mar','Apr','May','Jun',\n" \
507 "              'Jul','Aug','Sep','Oct','Nov','Dec'];\n" \
508 "\n" \
509 "function li_mon_num(mon) {\n" \
510 " var i; for (i = 0; i < 12 && mon != li_mon[i]; ++i); return i;\n" \
511 "}\n" \
512 "\n" \
513 "function li_date_cmp(s1, s2) {\n" \
514 " var dp1 = li_date_regex.exec(s1)\n" \
515 " var dp2 = li_date_regex.exec(s2)\n" \
516 " for (var i = 1; i < 7; ++i) {\n" \
517 "  var cmp = (2 != i)\n" \
518 "   ? parseInt(dp1[i]) - parseInt(dp2[i])\n" \
519 "   : li_mon_num(dp1[2]) - li_mon_num(dp2[2]);\n" \
520 "  if (0 != cmp) return cmp;\n" \
521 " }\n" \
522 " return 0;\n" \
523 "}\n" \
524 "\n" \
525 "function sortfn_then_by_name(a,b,sort_column) {\n" \
526 " if (sort_column == name_column || sort_column == type_column) {\n" \
527 "  var ad = (a.cells[type_column].innerHTML === 'Directory');\n" \
528 "  var bd = (b.cells[type_column].innerHTML === 'Directory');\n" \
529 "  if (ad != bd) return (ad ? -1 : 1);\n" \
530 " }\n" \
531 " var at = get_inner_text(a.cells[sort_column]);\n" \
532 " var bt = get_inner_text(b.cells[sort_column]);\n" \
533 " var cmp;\n" \
534 " if (sort_column == name_column) {\n" \
535 "  if (at == '..') return -1;\n" \
536 "  if (bt == '..') return  1;\n" \
537 " }\n" \
538 " if (a.cells[sort_column].className == 'int') {\n" \
539 "  cmp = parseInt(at)-parseInt(bt);\n" \
540 " } else if (sort_column == date_column) {\n" \
541 "  var ad = isdigit(at.substr(0,1));\n" \
542 "  var bd = isdigit(bt.substr(0,1));\n" \
543 "  if (ad != bd) return (!ad ? -1 : 1);\n" \
544 "  cmp = li_date_cmp(at,bt);\n" \
545 " } else if (sort_column == size_column) {\n" \
546 "  var ai = parseInt(at, 10) * unit_multiplier(at.substr(-1,1));\n" \
547 "  var bi = parseInt(bt, 10) * unit_multiplier(bt.substr(-1,1));\n" \
548 "  if (at.substr(0,1) == '-') ai = -1;\n" \
549 "  if (bt.substr(0,1) == '-') bi = -1;\n" \
550 "  cmp = ai - bi;\n" \
551 " } else {\n" \
552 "  cmp = at.toLocaleUpperCase().localeCompare(bt.toLocaleUpperCase());\n" \
553 "  if (0 != cmp) return cmp;\n" \
554 "  cmp = at.localeCompare(bt);\n" \
555 " }\n" \
556 " if (0 != cmp || sort_column == name_column) return cmp;\n" \
557 " return sortfn_then_by_name(a,b,name_column);\n" \
558 "}\n" \
559 "\n" \
560 "function sortfn(a,b) {\n" \
561 " return sortfn_then_by_name(a,b,click_column);\n" \
562 "}\n" \
563 "\n" \
564 "function resort(lnk) {\n" \
565 " var span = lnk.childNodes[1];\n" \
566 " var table = lnk.parentNode.parentNode.parentNode.parentNode;\n" \
567 " var rows = new Array();\n" \
568 " for (j=1;j<table.rows.length;j++)\n" \
569 "  rows[j-1] = table.rows[j];\n" \
570 " click_column = lnk.parentNode.cellIndex;\n" \
571 " rows.sort(sortfn);\n" \
572 "\n" \
573 " if (prev_span != null) prev_span.innerHTML = '';\n" \
574 " if (span.getAttribute('sortdir')=='down') {\n" \
575 "  span.innerHTML = '&uarr;';\n" \
576 "  span.setAttribute('sortdir','up');\n" \
577 "  rows.reverse();\n" \
578 " } else {\n" \
579 "  span.innerHTML = '&darr;';\n" \
580 "  span.setAttribute('sortdir','down');\n" \
581 " }\n" \
582 " for (i=0;i<rows.length;i++)\n" \
583 "  table.tBodies[0].appendChild(rows[i]);\n" \
584 " prev_span = span;\n" \
585 "}\n";
586 
587 /* portions copied from mod_dirlist (lighttpd2) */
588 static const char js_simple_table_init_sort[] = \
589 "\n" \
590 "function init_sort(init_sort_column, ascending) {\n" \
591 " var tables = document.getElementsByTagName(\"table\");\n" \
592 " for (var i = 0; i < tables.length; i++) {\n" \
593 "  var table = tables[i];\n" \
594 "  //var c = table.getAttribute(\"class\")\n" \
595 "  //if (-1 != c.split(\" \").indexOf(\"sort\")) {\n" \
596 "   var row = table.rows[0].cells;\n" \
597 "   for (var j = 0; j < row.length; j++) {\n" \
598 "    var n = row[j];\n" \
599 "    if (n.childNodes.length == 1 && n.childNodes[0].nodeType == 3) {\n" \
600 "     var link = document.createElement(\"a\");\n" \
601 "     var title = n.childNodes[0].nodeValue.replace(/:$/, \"\");\n" \
602 "     link.appendChild(document.createTextNode(title));\n" \
603 "     link.setAttribute(\"href\", \"#\");\n" \
604 "     link.setAttribute(\"class\", \"sortheader\");\n" \
605 "     link.setAttribute(\"onclick\", \"resort(this);return false;\");\n" \
606 "     var arrow = document.createElement(\"span\");\n" \
607 "     arrow.setAttribute(\"class\", \"sortarrow\");\n" \
608 "     arrow.appendChild(document.createTextNode(\":\"));\n" \
609 "     link.appendChild(arrow)\n" \
610 "     n.replaceChild(link, n.firstChild);\n" \
611 "    }\n" \
612 "   }\n" \
613 "   var lnk = row[init_sort_column].firstChild;\n" \
614 "   if (ascending) {\n" \
615 "    var span = lnk.childNodes[1];\n" \
616 "    span.setAttribute('sortdir','down');\n" \
617 "   }\n" \
618 "   resort(lnk);\n" \
619 "  //}\n" \
620 " }\n" \
621 "}\n";
622 
623 static void http_dirlist_append_js_table_resort (buffer * const b, const request_st * const r) {
624 	char col = '0';
625 	char ascending = '0';
626 	if (!buffer_string_is_empty(&r->uri.query)) {
627 		const char *qs = r->uri.query.ptr;
628 		do {
629 			if (qs[0] == 'C' && qs[1] == '=') {
630 				switch (qs[2]) {
631 				case 'N': col = '0'; break;
632 				case 'M': col = '1'; break;
633 				case 'S': col = '2'; break;
634 				case 'T':
635 				case 'D': col = '3'; break;
636 				default:  break;
637 				}
638 			}
639 			else if (qs[0] == 'O' && qs[1] == '=') {
640 				switch (qs[2]) {
641 				case 'A': ascending = '1'; break;
642 				case 'D': ascending = '0'; break;
643 				default:  break;
644 				}
645 			}
646 		} while ((qs = strchr(qs, '&')) && *++qs);
647 	}
648 
649 	buffer_append_string_len(b, CONST_STR_LEN("\n<script type=\"text/javascript\">\n// <!--\n\n"));
650 	buffer_append_string_len(b, js_simple_table_resort, sizeof(js_simple_table_resort)-1);
651 	buffer_append_string_len(b, js_simple_table_init_sort, sizeof(js_simple_table_init_sort)-1);
652 	buffer_append_string_len(b, CONST_STR_LEN("\ninit_sort("));
653 	buffer_append_string_len(b, &col, 1);
654 	buffer_append_string_len(b, CONST_STR_LEN(", "));
655 	buffer_append_string_len(b, &ascending, 1);
656 	buffer_append_string_len(b, CONST_STR_LEN(");\n\n// -->\n</script>\n\n"));
657 }
658 
659 static void http_list_directory_header(const request_st * const r, plugin_data * const p, buffer * const out) {
660 
661 	if (p->conf.auto_layout) {
662 		buffer_append_string_len(out, CONST_STR_LEN(
663 			"<!DOCTYPE html>\n"
664 			"<html>\n"
665 			"<head>\n"
666 		));
667 		if (!buffer_string_is_empty(p->conf.encoding)) {
668 			buffer_append_string_len(out, CONST_STR_LEN("<meta charset=\""));
669 			buffer_append_string_buffer(out, p->conf.encoding);
670 			buffer_append_string_len(out, CONST_STR_LEN("\">\n"));
671 		}
672 		buffer_append_string_len(out, CONST_STR_LEN("<title>Index of "));
673 		buffer_append_string_encoded(out, CONST_BUF_LEN(&r->uri.path), ENCODING_MINIMAL_XML);
674 		buffer_append_string_len(out, CONST_STR_LEN("</title>\n"));
675 
676 		if (!buffer_string_is_empty(p->conf.external_css)) {
677 			buffer_append_string_len(out, CONST_STR_LEN("<meta name=\"viewport\" content=\"initial-scale=1\">"));
678 			buffer_append_string_len(out, CONST_STR_LEN("<link rel=\"stylesheet\" type=\"text/css\" href=\""));
679 			buffer_append_string_buffer(out, p->conf.external_css);
680 			buffer_append_string_len(out, CONST_STR_LEN("\">\n"));
681 		} else {
682 			buffer_append_string_len(out, CONST_STR_LEN(
683 				"<style type=\"text/css\">\n"
684 				"a, a:active {text-decoration: none; color: blue;}\n"
685 				"a:visited {color: #48468F;}\n"
686 				"a:hover, a:focus {text-decoration: underline; color: red;}\n"
687 				"body {background-color: #F5F5F5;}\n"
688 				"h2 {margin-bottom: 12px;}\n"
689 				"table {margin-left: 12px;}\n"
690 				"th, td {"
691 				" font: 90% monospace;"
692 				" text-align: left;"
693 				"}\n"
694 				"th {"
695 				" font-weight: bold;"
696 				" padding-right: 14px;"
697 				" padding-bottom: 3px;"
698 				"}\n"
699 				"td {padding-right: 14px;}\n"
700 				"td.s, th.s {text-align: right;}\n"
701 				"div.list {"
702 				" background-color: white;"
703 				" border-top: 1px solid #646464;"
704 				" border-bottom: 1px solid #646464;"
705 				" padding-top: 10px;"
706 				" padding-bottom: 14px;"
707 				"}\n"
708 				"div.foot {"
709 				" font: 90% monospace;"
710 				" color: #787878;"
711 				" padding-top: 4px;"
712 				"}\n"
713 				"</style>\n"
714 			));
715 		}
716 
717 		buffer_append_string_len(out, CONST_STR_LEN("</head>\n<body>\n"));
718 	}
719 
720 	if (!buffer_string_is_empty(p->conf.show_header)) {
721 		/* if we have a HEADER file, display it in <pre class="header"></pre> */
722 
723 		const buffer *hb = p->conf.show_header;
724 		if (hb->ptr[0] != '/') {
725 			hb = &p->tmp_buf;
726 			buffer_copy_buffer(&p->tmp_buf, &r->physical.path);
727 			buffer_append_path_len(&p->tmp_buf, CONST_BUF_LEN(p->conf.show_header));
728 		}
729 
730 		http_list_directory_include_file(out, r->conf.follow_symlink, hb, "header", p->conf.encode_header);
731 	}
732 
733 	buffer_append_string_len(out, CONST_STR_LEN("<h2>Index of "));
734 	buffer_append_string_encoded(out, CONST_BUF_LEN(&r->uri.path), ENCODING_MINIMAL_XML);
735 	buffer_append_string_len(out, CONST_STR_LEN(
736 		"</h2>\n"
737 		"<div class=\"list\">\n"
738 		"<table summary=\"Directory Listing\" cellpadding=\"0\" cellspacing=\"0\">\n"
739 		"<thead>"
740 		"<tr>"
741 			"<th class=\"n\">Name</th>"
742 			"<th class=\"m\">Last Modified</th>"
743 			"<th class=\"s\">Size</th>"
744 			"<th class=\"t\">Type</th>"
745 		"</tr>"
746 		"</thead>\n"
747 		"<tbody>\n"
748 	));
749 	if (!buffer_is_equal_string(&r->uri.path, CONST_STR_LEN("/"))) {
750 		buffer_append_string_len(out, CONST_STR_LEN(
751 		"<tr class=\"d\">"
752 			"<td class=\"n\"><a href=\"../\">..</a>/</td>"
753 			"<td class=\"m\">&nbsp;</td>"
754 			"<td class=\"s\">- &nbsp;</td>"
755 			"<td class=\"t\">Directory</td>"
756 		"</tr>\n"
757 		));
758 	}
759 }
760 
761 static void http_list_directory_footer(const request_st * const r, plugin_data * const p, buffer * const out) {
762 
763 	buffer_append_string_len(out, CONST_STR_LEN(
764 		"</tbody>\n"
765 		"</table>\n"
766 		"</div>\n"
767 	));
768 
769 	if (!buffer_string_is_empty(p->conf.show_readme)) {
770 		/* if we have a README file, display it in <pre class="readme"></pre> */
771 
772 		const buffer *rb = p->conf.show_readme;
773 		if (rb->ptr[0] != '/') {
774 			rb = &p->tmp_buf;
775 			buffer_copy_buffer(&p->tmp_buf, &r->physical.path);
776 			buffer_append_path_len(&p->tmp_buf, CONST_BUF_LEN(p->conf.show_readme));
777 		}
778 
779 		http_list_directory_include_file(out, r->conf.follow_symlink, rb, "readme", p->conf.encode_readme);
780 	}
781 
782 	if(p->conf.auto_layout) {
783 
784 		buffer_append_string_len(out, CONST_STR_LEN(
785 			"<div class=\"foot\">"
786 		));
787 
788 		if (!buffer_string_is_empty(p->conf.set_footer)) {
789 			buffer_append_string_buffer(out, p->conf.set_footer);
790 		} else {
791 			buffer_append_string_buffer(out, r->conf.server_tag);
792 		}
793 
794 		buffer_append_string_len(out, CONST_STR_LEN(
795 			"</div>\n"
796 		));
797 
798 		if (!buffer_string_is_empty(p->conf.external_js)) {
799 			buffer_append_string_len(out, CONST_STR_LEN("<script type=\"text/javascript\" src=\""));
800 			buffer_append_string_buffer(out, p->conf.external_js);
801 			buffer_append_string_len(out, CONST_STR_LEN("\"></script>\n"));
802 		} else if (buffer_is_empty(p->conf.external_js)) {
803 			http_dirlist_append_js_table_resort(out, r);
804 		}
805 
806 		buffer_append_string_len(out, CONST_STR_LEN(
807 			"</body>\n"
808 			"</html>\n"
809 		));
810 	}
811 }
812 
813 static int http_list_directory(request_st * const r, plugin_data * const p, buffer * const dir) {
814 	DIR *dp;
815 	buffer *out;
816 	struct dirent *dent;
817 	struct stat st;
818 	char *path, *path_file;
819 	size_t i;
820 	int hide_dotfiles = p->conf.hide_dot_files;
821 	dirls_list_t dirs, files, *list;
822 	dirls_entry_t *tmp;
823 	char sizebuf[sizeof("999.9K")];
824 	char datebuf[sizeof("2005-Jan-01 22:23:24")];
825 	const buffer *content_type;
826 	long name_max;
827 	log_error_st * const errh = r->conf.errh;
828 #ifdef HAVE_LOCALTIME_R
829 	struct tm tm;
830 #endif
831 
832 	if (buffer_string_is_empty(dir)) return -1;
833 
834 	i = buffer_string_length(dir);
835 
836 #ifdef HAVE_PATHCONF
837 	if (0 >= (name_max = pathconf(dir->ptr, _PC_NAME_MAX))) {
838 		/* some broken fs (fuse) return 0 instead of -1 */
839 #ifdef NAME_MAX
840 		name_max = NAME_MAX;
841 #else
842 		name_max = 255; /* stupid default */
843 #endif
844 	}
845 #elif defined __WIN32
846 	name_max = FILENAME_MAX;
847 #else
848 	name_max = NAME_MAX;
849 #endif
850 
851 	path = malloc(i + name_max + 1);
852 	force_assert(NULL != path);
853 	memcpy(path, dir->ptr, i+1);
854 	path_file = path + i;
855 
856 	if (NULL == (dp = opendir(path))) {
857 		log_error(errh, __FILE__, __LINE__,
858 		  "opendir failed: %s", dir->ptr);
859 
860 		free(path);
861 		return -1;
862 	}
863 
864 	dirs.ent   = (dirls_entry_t**) malloc(sizeof(dirls_entry_t*) * DIRLIST_BLOB_SIZE);
865 	force_assert(dirs.ent);
866 	dirs.size  = DIRLIST_BLOB_SIZE;
867 	dirs.used  = 0;
868 	files.ent  = (dirls_entry_t**) malloc(sizeof(dirls_entry_t*) * DIRLIST_BLOB_SIZE);
869 	force_assert(files.ent);
870 	files.size = DIRLIST_BLOB_SIZE;
871 	files.used = 0;
872 
873 	while ((dent = readdir(dp)) != NULL) {
874 		if (dent->d_name[0] == '.') {
875 			if (hide_dotfiles)
876 				continue;
877 			if (dent->d_name[1] == '\0')
878 				continue;
879 			if (dent->d_name[1] == '.' && dent->d_name[2] == '\0')
880 				continue;
881 		}
882 
883 		if (p->conf.hide_readme_file && !buffer_string_is_empty(p->conf.show_readme)) {
884 			if (strcmp(dent->d_name, p->conf.show_readme->ptr) == 0)
885 				continue;
886 		}
887 		if (p->conf.hide_header_file && !buffer_string_is_empty(p->conf.show_header)) {
888 			if (strcmp(dent->d_name, p->conf.show_header->ptr) == 0)
889 				continue;
890 		}
891 
892 		i = strlen(dent->d_name);
893 
894 		/* compare d_name against excludes array
895 		 * elements, skipping any that match.
896 		 */
897 		if (p->conf.excludes
898 		    && mod_dirlisting_exclude(errh, p->conf.excludes, dent->d_name, i))
899 			continue;
900 
901 		/* NOTE: the manual says, d_name is never more than NAME_MAX
902 		 *       so this should actually not be a buffer-overflow-risk
903 		 */
904 		if (i > (size_t)name_max) continue;
905 
906 		memcpy(path_file, dent->d_name, i + 1);
907 		if (stat(path, &st) != 0)
908 			continue;
909 
910 		list = &files;
911 		if (S_ISDIR(st.st_mode))
912 			list = &dirs;
913 
914 		if (list->used == list->size) {
915 			list->size += DIRLIST_BLOB_SIZE;
916 			list->ent   = (dirls_entry_t**) realloc(list->ent, sizeof(dirls_entry_t*) * list->size);
917 			force_assert(list->ent);
918 		}
919 
920 		tmp = (dirls_entry_t*) malloc(sizeof(dirls_entry_t) + 1 + i);
921 		tmp->mtime = st.st_mtime;
922 		tmp->size  = st.st_size;
923 		tmp->namelen = (uint32_t)i;
924 		memcpy(DIRLIST_ENT_NAME(tmp), dent->d_name, i + 1);
925 
926 		list->ent[list->used++] = tmp;
927 	}
928 	closedir(dp);
929 
930 	if (dirs.used) http_dirls_sort(dirs.ent, dirs.used);
931 
932 	if (files.used) http_dirls_sort(files.ent, files.used);
933 
934 	out = chunkqueue_append_buffer_open(r->write_queue);
935 	http_list_directory_header(r, p, out);
936 
937 	/* directories */
938 	for (i = 0; i < dirs.used; i++) {
939 		tmp = dirs.ent[i];
940 
941 #ifdef HAVE_LOCALTIME_R
942 		localtime_r(&(tmp->mtime), &tm);
943 		strftime(datebuf, sizeof(datebuf), "%Y-%b-%d %H:%M:%S", &tm);
944 #else
945 		strftime(datebuf, sizeof(datebuf), "%Y-%b-%d %H:%M:%S", localtime(&(tmp->mtime)));
946 #endif
947 
948 		buffer_append_string_len(out, CONST_STR_LEN("<tr class=\"d\"><td class=\"n\"><a href=\""));
949 		buffer_append_string_encoded(out, DIRLIST_ENT_NAME(tmp), tmp->namelen, ENCODING_REL_URI_PART);
950 		buffer_append_string_len(out, CONST_STR_LEN("/\">"));
951 		buffer_append_string_encoded(out, DIRLIST_ENT_NAME(tmp), tmp->namelen, ENCODING_MINIMAL_XML);
952 		buffer_append_string_len(out, CONST_STR_LEN("</a>/</td><td class=\"m\">"));
953 		buffer_append_string_len(out, datebuf, sizeof(datebuf) - 1);
954 		buffer_append_string_len(out, CONST_STR_LEN("</td><td class=\"s\">- &nbsp;</td><td class=\"t\">Directory</td></tr>\n"));
955 
956 		free(tmp);
957 	}
958 
959 	/* files */
960 	const array * const mimetypes = r->conf.mimetypes;
961 	for (i = 0; i < files.used; i++) {
962 		tmp = files.ent[i];
963 
964 	  #if defined(HAVE_XATTR) || defined(HAVE_EXTATTR) /*(pass full path)*/
965 		content_type = NULL;
966 		if (r->conf.use_xattr) {
967 			memcpy(path_file, DIRLIST_ENT_NAME(tmp), tmp->namelen + 1);
968 			content_type = stat_cache_mimetype_by_xattr(path);
969 		}
970 		if (NULL == content_type)
971 	  #endif
972 			content_type = stat_cache_mimetype_by_ext(mimetypes, DIRLIST_ENT_NAME(tmp), tmp->namelen);
973 		if (NULL == content_type) {
974 			static const buffer octet_stream =
975 			  { "application/octet-stream",
976 			    sizeof("application/octet-stream"), 0 };
977 			content_type = &octet_stream;
978 		}
979 
980 #ifdef HAVE_LOCALTIME_R
981 		localtime_r(&(tmp->mtime), &tm);
982 		strftime(datebuf, sizeof(datebuf), "%Y-%b-%d %H:%M:%S", &tm);
983 #else
984 		strftime(datebuf, sizeof(datebuf), "%Y-%b-%d %H:%M:%S", localtime(&(tmp->mtime)));
985 #endif
986 		http_list_directory_sizefmt(sizebuf, sizeof(sizebuf), tmp->size);
987 
988 		buffer_append_string_len(out, CONST_STR_LEN("<tr><td class=\"n\"><a href=\""));
989 		buffer_append_string_encoded(out, DIRLIST_ENT_NAME(tmp), tmp->namelen, ENCODING_REL_URI_PART);
990 		buffer_append_string_len(out, CONST_STR_LEN("\">"));
991 		buffer_append_string_encoded(out, DIRLIST_ENT_NAME(tmp), tmp->namelen, ENCODING_MINIMAL_XML);
992 		buffer_append_string_len(out, CONST_STR_LEN("</a></td><td class=\"m\">"));
993 		buffer_append_string_len(out, datebuf, sizeof(datebuf) - 1);
994 		buffer_append_string_len(out, CONST_STR_LEN("</td><td class=\"s\">"));
995 		buffer_append_string(out, sizebuf);
996 		buffer_append_string_len(out, CONST_STR_LEN("</td><td class=\"t\">"));
997 		buffer_append_string_buffer(out, content_type);
998 		buffer_append_string_len(out, CONST_STR_LEN("</td></tr>\n"));
999 
1000 		free(tmp);
1001 	}
1002 
1003 	free(files.ent);
1004 	free(dirs.ent);
1005 	free(path);
1006 
1007 	http_list_directory_footer(r, p, out);
1008 
1009 	/* Insert possible charset to Content-Type */
1010 	if (buffer_string_is_empty(p->conf.encoding)) {
1011 		http_header_response_set(r, HTTP_HEADER_CONTENT_TYPE, CONST_STR_LEN("Content-Type"), CONST_STR_LEN("text/html"));
1012 	} else {
1013 		buffer_copy_string_len(&p->tmp_buf, CONST_STR_LEN("text/html; charset="));
1014 		buffer_append_string_buffer(&p->tmp_buf, p->conf.encoding);
1015 		http_header_response_set(r, HTTP_HEADER_CONTENT_TYPE, CONST_STR_LEN("Content-Type"), CONST_BUF_LEN(&p->tmp_buf));
1016 	}
1017 
1018 	chunkqueue_append_buffer_commit(r->write_queue);
1019 	r->resp_body_finished = 1;
1020 
1021 	return 0;
1022 }
1023 
1024 
1025 
1026 URIHANDLER_FUNC(mod_dirlisting_subrequest) {
1027 	plugin_data *p = p_d;
1028 	stat_cache_entry *sce = NULL;
1029 
1030 	if (NULL != r->handler_module) return HANDLER_GO_ON;
1031 	if (buffer_string_is_empty(&r->uri.path)) return HANDLER_GO_ON;
1032 	if (r->uri.path.ptr[buffer_string_length(&r->uri.path) - 1] != '/') return HANDLER_GO_ON;
1033 	if (!http_method_get_or_head(r->http_method)) return HANDLER_GO_ON;
1034 	if (buffer_is_empty(&r->physical.path)) return HANDLER_GO_ON;
1035 
1036 	mod_dirlisting_patch_config(r, p);
1037 
1038 	if (!p->conf.dir_listing) return HANDLER_GO_ON;
1039 
1040 	if (r->conf.log_request_handling) {
1041 		log_error(r->conf.errh, __FILE__, __LINE__,
1042 		  "-- handling the request as Dir-Listing");
1043 		log_error(r->conf.errh, __FILE__, __LINE__,
1044 		  "URI          : %s", r->uri.path.ptr);
1045 	}
1046 
1047 	sce = stat_cache_get_entry(&r->physical.path);
1048 	if (NULL == sce) {
1049 		log_error(r->conf.errh, __FILE__, __LINE__,
1050 		  "stat_cache_get_entry failed: %s", r->physical.path.ptr);
1051 		r->http_status = 500;
1052 		return HANDLER_FINISHED;
1053 	}
1054 
1055 	if (!S_ISDIR(sce->st.st_mode)) return HANDLER_GO_ON;
1056 
1057 	if (http_list_directory(r, p, &r->physical.path)) {
1058 		/* dirlisting failed */
1059 		r->http_status = 403;
1060 	}
1061 
1062 	buffer_reset(&r->physical.path);
1063 
1064 	/* not found */
1065 	return HANDLER_FINISHED;
1066 }
1067 
1068 
1069 int mod_dirlisting_plugin_init(plugin *p);
1070 int mod_dirlisting_plugin_init(plugin *p) {
1071 	p->version     = LIGHTTPD_VERSION_ID;
1072 	p->name        = "dirlisting";
1073 
1074 	p->init        = mod_dirlisting_init;
1075 	p->handle_subrequest_start  = mod_dirlisting_subrequest;
1076 	p->set_defaults  = mod_dirlisting_set_defaults;
1077 	p->cleanup     = mod_dirlisting_free;
1078 
1079 	return 0;
1080 }
1081