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