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