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