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