1 #include "first.h" 2 3 #include "base.h" 4 #include "fdevent.h" 5 #include "log.h" 6 #include "buffer.h" 7 #include "http_cgi.h" 8 #include "http_chunk.h" 9 #include "http_etag.h" 10 #include "http_header.h" 11 #include "stat_cache.h" 12 13 #include "plugin.h" 14 15 #include "response.h" 16 17 #include "mod_ssi.h" 18 19 #include "sys-socket.h" 20 #include "sys-time.h" 21 22 #include <sys/types.h> 23 #include <sys/stat.h> 24 #ifdef HAVE_SYS_WAIT_H 25 #include <sys/wait.h> 26 #endif 27 28 #include <ctype.h> 29 #include <stdlib.h> 30 #include <string.h> 31 #include <errno.h> 32 #include <fcntl.h> 33 #include <unistd.h> 34 35 #ifdef HAVE_PWD_H 36 # include <pwd.h> 37 #endif 38 39 #ifdef HAVE_SYS_FILIO_H 40 # include <sys/filio.h> 41 #endif 42 43 static handler_ctx * handler_ctx_init(plugin_data *p, log_error_st *errh) { 44 handler_ctx *hctx = calloc(1, sizeof(*hctx)); 45 force_assert(hctx); 46 hctx->errh = errh; 47 hctx->timefmt = &p->timefmt; 48 hctx->stat_fn = &p->stat_fn; 49 hctx->ssi_vars = p->ssi_vars; 50 hctx->ssi_cgi_env = p->ssi_cgi_env; 51 memcpy(&hctx->conf, &p->conf, sizeof(plugin_config)); 52 return hctx; 53 } 54 55 static void handler_ctx_free(handler_ctx *hctx) { 56 chunkqueue_reset(&hctx->wq); 57 free(hctx); 58 } 59 60 /* The newest modified time of included files for include statement */ 61 static volatile time_t include_file_last_mtime = 0; 62 63 INIT_FUNC(mod_ssi_init) { 64 plugin_data *p; 65 66 p = calloc(1, sizeof(*p)); 67 force_assert(p); 68 69 p->ssi_vars = array_init(8); 70 p->ssi_cgi_env = array_init(32); 71 72 return p; 73 } 74 75 FREE_FUNC(mod_ssi_free) { 76 plugin_data *p = p_d; 77 array_free(p->ssi_vars); 78 array_free(p->ssi_cgi_env); 79 free(p->timefmt.ptr); 80 free(p->stat_fn.ptr); 81 } 82 83 static void mod_ssi_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) { 84 switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */ 85 case 0: /* ssi.extension */ 86 pconf->ssi_extension = cpv->v.a; 87 break; 88 case 1: /* ssi.content-type */ 89 pconf->content_type = cpv->v.b; 90 break; 91 case 2: /* ssi.conditional-requests */ 92 pconf->conditional_requests = cpv->v.u; 93 break; 94 case 3: /* ssi.exec */ 95 pconf->ssi_exec = cpv->v.u; 96 break; 97 case 4: /* ssi.recursion-max */ 98 pconf->ssi_recursion_max = cpv->v.shrt; 99 break; 100 default:/* should not happen */ 101 return; 102 } 103 } 104 105 static void mod_ssi_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) { 106 do { 107 mod_ssi_merge_config_cpv(pconf, cpv); 108 } while ((++cpv)->k_id != -1); 109 } 110 111 static void mod_ssi_patch_config(request_st * const r, plugin_data * const p) { 112 memcpy(&p->conf, &p->defaults, sizeof(plugin_config)); 113 for (int i = 1, used = p->nconfig; i < used; ++i) { 114 if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id)) 115 mod_ssi_merge_config(&p->conf, p->cvlist + p->cvlist[i].v.u2[0]); 116 } 117 } 118 119 SETDEFAULTS_FUNC(mod_ssi_set_defaults) { 120 static const config_plugin_keys_t cpk[] = { 121 { CONST_STR_LEN("ssi.extension"), 122 T_CONFIG_ARRAY_VLIST, 123 T_CONFIG_SCOPE_CONNECTION } 124 ,{ CONST_STR_LEN("ssi.content-type"), 125 T_CONFIG_STRING, 126 T_CONFIG_SCOPE_CONNECTION } 127 ,{ CONST_STR_LEN("ssi.conditional-requests"), 128 T_CONFIG_BOOL, 129 T_CONFIG_SCOPE_CONNECTION } 130 ,{ CONST_STR_LEN("ssi.exec"), 131 T_CONFIG_BOOL, 132 T_CONFIG_SCOPE_CONNECTION } 133 ,{ CONST_STR_LEN("ssi.recursion-max"), 134 T_CONFIG_SHORT, 135 T_CONFIG_SCOPE_CONNECTION } 136 ,{ NULL, 0, 137 T_CONFIG_UNSET, 138 T_CONFIG_SCOPE_UNSET } 139 }; 140 141 plugin_data * const p = p_d; 142 if (!config_plugin_values_init(srv, p, cpk, "mod_ssi")) 143 return HANDLER_ERROR; 144 145 p->defaults.ssi_exec = 1; 146 147 /* initialize p->defaults from global config context */ 148 if (p->nconfig > 0 && p->cvlist->v.u2[1]) { 149 const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0]; 150 if (-1 != cpv->k_id) 151 mod_ssi_merge_config(&p->defaults, cpv); 152 } 153 154 return HANDLER_GO_ON; 155 } 156 157 158 static int ssi_env_add(void *venv, const char *key, size_t klen, const char *val, size_t vlen) { 159 array_set_key_value((array *)venv, key, klen, val, vlen); 160 return 0; 161 } 162 163 static int build_ssi_cgi_vars(request_st * const r, handler_ctx * const p) { 164 http_cgi_opts opts = { 0, 0, NULL, NULL }; 165 /* temporarily remove Authorization from request headers 166 * so that Authorization does not end up in SSI environment */ 167 buffer *vb_auth = http_header_request_get(r, HTTP_HEADER_AUTHORIZATION, CONST_STR_LEN("Authorization")); 168 buffer b_auth; 169 if (vb_auth) { 170 memcpy(&b_auth, vb_auth, sizeof(buffer)); 171 memset(vb_auth, 0, sizeof(buffer)); 172 } 173 174 array_reset_data_strings(p->ssi_cgi_env); 175 176 if (0 != http_cgi_headers(r, &opts, ssi_env_add, p->ssi_cgi_env)) { 177 r->http_status = 400; 178 return -1; 179 } 180 181 if (vb_auth) { 182 memcpy(vb_auth, &b_auth, sizeof(buffer)); 183 } 184 185 return 0; 186 } 187 188 static void mod_ssi_timefmt (buffer * const b, buffer *timefmtb, time_t t, int localtm) { 189 struct tm tm; 190 const char * const timefmt = buffer_string_is_empty(timefmtb) 191 ? "%a, %d %b %Y %T %Z" 192 : timefmtb->ptr; 193 buffer_append_strftime(b, timefmt, 194 localtm ? localtime_r(&t, &tm) : gmtime_r(&t, &tm)); 195 if (buffer_string_is_empty(b)) 196 buffer_copy_string_len(b, CONST_STR_LEN("(none)")); 197 } 198 199 static int mod_ssi_process_file(request_st *r, handler_ctx *p, struct stat *st); 200 201 static int process_ssi_stmt(request_st * const r, handler_ctx * const p, const char ** const l, size_t n, struct stat * const st) { 202 203 /** 204 * <!--#element attribute=value attribute=value ... --> 205 * 206 * config DONE 207 * errmsg -- missing 208 * sizefmt DONE 209 * timefmt DONE 210 * echo DONE 211 * var DONE 212 * encoding -- missing 213 * exec DONE 214 * cgi -- never 215 * cmd DONE 216 * fsize DONE 217 * file DONE 218 * virtual DONE 219 * flastmod DONE 220 * file DONE 221 * virtual DONE 222 * include DONE 223 * file DONE 224 * virtual DONE 225 * printenv DONE 226 * set DONE 227 * var DONE 228 * value DONE 229 * 230 * if DONE 231 * elif DONE 232 * else DONE 233 * endif DONE 234 * 235 * 236 * expressions 237 * AND, OR DONE 238 * comp DONE 239 * ${...} -- missing 240 * $... DONE 241 * '...' DONE 242 * ( ... ) DONE 243 * 244 * 245 * 246 * ** all DONE ** 247 * DATE_GMT 248 * The current date in Greenwich Mean Time. 249 * DATE_LOCAL 250 * The current date in the local time zone. 251 * DOCUMENT_NAME 252 * The filename (excluding directories) of the document requested by the user. 253 * DOCUMENT_URI 254 * The (%-decoded) URL path of the document requested by the user. Note that in the case of nested include files, this is not then URL for the current document. 255 * LAST_MODIFIED 256 * The last modification date of the document requested by the user. 257 * USER_NAME 258 * Contains the owner of the file which included it. 259 * 260 */ 261 262 size_t i, ssicmd = 0; 263 buffer *tb = NULL; 264 265 static const struct { 266 const char *var; 267 enum { SSI_UNSET, SSI_ECHO, SSI_FSIZE, SSI_INCLUDE, SSI_FLASTMOD, 268 SSI_CONFIG, SSI_PRINTENV, SSI_SET, SSI_IF, SSI_ELIF, 269 SSI_ELSE, SSI_ENDIF, SSI_EXEC, SSI_COMMENT } type; 270 } ssicmds[] = { 271 { "echo", SSI_ECHO }, 272 { "include", SSI_INCLUDE }, 273 { "flastmod", SSI_FLASTMOD }, 274 { "fsize", SSI_FSIZE }, 275 { "config", SSI_CONFIG }, 276 { "printenv", SSI_PRINTENV }, 277 { "set", SSI_SET }, 278 { "if", SSI_IF }, 279 { "elif", SSI_ELIF }, 280 { "endif", SSI_ENDIF }, 281 { "else", SSI_ELSE }, 282 { "exec", SSI_EXEC }, 283 { "comment", SSI_COMMENT }, 284 285 { NULL, SSI_UNSET } 286 }; 287 288 for (i = 0; ssicmds[i].var; i++) { 289 if (0 == strcmp(l[1], ssicmds[i].var)) { 290 ssicmd = ssicmds[i].type; 291 break; 292 } 293 } 294 295 chunkqueue * const cq = &p->wq; 296 297 switch(ssicmd) { 298 case SSI_ECHO: { 299 /* echo */ 300 int var = 0; 301 /* int enc = 0; */ 302 const char *var_val = NULL; 303 304 static const struct { 305 const char *var; 306 enum { 307 SSI_ECHO_UNSET, 308 SSI_ECHO_DATE_GMT, 309 SSI_ECHO_DATE_LOCAL, 310 SSI_ECHO_DOCUMENT_NAME, 311 SSI_ECHO_DOCUMENT_URI, 312 SSI_ECHO_LAST_MODIFIED, 313 SSI_ECHO_USER_NAME, 314 SSI_ECHO_SCRIPT_URI, 315 SSI_ECHO_SCRIPT_URL, 316 } type; 317 } echovars[] = { 318 { "DATE_GMT", SSI_ECHO_DATE_GMT }, 319 { "DATE_LOCAL", SSI_ECHO_DATE_LOCAL }, 320 { "DOCUMENT_NAME", SSI_ECHO_DOCUMENT_NAME }, 321 { "DOCUMENT_URI", SSI_ECHO_DOCUMENT_URI }, 322 { "LAST_MODIFIED", SSI_ECHO_LAST_MODIFIED }, 323 { "USER_NAME", SSI_ECHO_USER_NAME }, 324 { "SCRIPT_URI", SSI_ECHO_SCRIPT_URI }, 325 { "SCRIPT_URL", SSI_ECHO_SCRIPT_URL }, 326 327 { NULL, SSI_ECHO_UNSET } 328 }; 329 330 /* 331 static const struct { 332 const char *var; 333 enum { SSI_ENC_UNSET, SSI_ENC_URL, SSI_ENC_NONE, SSI_ENC_ENTITY } type; 334 } encvars[] = { 335 { "url", SSI_ENC_URL }, 336 { "none", SSI_ENC_NONE }, 337 { "entity", SSI_ENC_ENTITY }, 338 339 { NULL, SSI_ENC_UNSET } 340 }; 341 */ 342 343 for (i = 2; i < n; i += 2) { 344 if (0 == strcmp(l[i], "var")) { 345 int j; 346 347 var_val = l[i+1]; 348 349 for (j = 0; echovars[j].var; j++) { 350 if (0 == strcmp(l[i+1], echovars[j].var)) { 351 var = echovars[j].type; 352 break; 353 } 354 } 355 } else if (0 == strcmp(l[i], "encoding")) { 356 /* 357 int j; 358 359 for (j = 0; encvars[j].var; j++) { 360 if (0 == strcmp(l[i+1], encvars[j].var)) { 361 enc = encvars[j].type; 362 break; 363 } 364 } 365 */ 366 } else { 367 log_error(r->conf.errh, __FILE__, __LINE__, 368 "ssi: unknown attribute for %s %s", l[1], l[i]); 369 } 370 } 371 372 if (p->if_is_false) break; 373 374 if (!var_val) { 375 log_error(r->conf.errh, __FILE__, __LINE__, 376 "ssi: %s var is missing", l[1]); 377 break; 378 } 379 380 switch(var) { 381 case SSI_ECHO_USER_NAME: { 382 tb = r->tmp_buf; 383 buffer_clear(tb); 384 #ifdef HAVE_PWD_H 385 struct passwd *pw; 386 if (NULL == (pw = getpwuid(st->st_uid))) { 387 buffer_append_int(tb, st->st_uid); 388 } else { 389 buffer_copy_string(tb, pw->pw_name); 390 } 391 #else 392 buffer_append_int(tb, st->st_uid); 393 #endif 394 chunkqueue_append_mem(cq, CONST_BUF_LEN(tb)); 395 break; 396 } 397 case SSI_ECHO_LAST_MODIFIED: 398 case SSI_ECHO_DATE_LOCAL: 399 case SSI_ECHO_DATE_GMT: 400 tb = r->tmp_buf; 401 buffer_clear(tb); 402 mod_ssi_timefmt(tb, p->timefmt, 403 (var == SSI_ECHO_LAST_MODIFIED) 404 ? st->st_mtime 405 : log_epoch_secs, 406 (var != SSI_ECHO_DATE_GMT)); 407 chunkqueue_append_mem(cq, CONST_BUF_LEN(tb)); 408 break; 409 case SSI_ECHO_DOCUMENT_NAME: { 410 char *sl; 411 412 if (NULL == (sl = strrchr(r->physical.path.ptr, '/'))) { 413 chunkqueue_append_mem(cq, CONST_BUF_LEN(&r->physical.path)); 414 } else { 415 chunkqueue_append_mem(cq, sl + 1, strlen(sl + 1)); 416 } 417 break; 418 } 419 case SSI_ECHO_DOCUMENT_URI: { 420 chunkqueue_append_mem(cq, CONST_BUF_LEN(&r->uri.path)); 421 break; 422 } 423 case SSI_ECHO_SCRIPT_URI: { 424 if (!buffer_string_is_empty(&r->uri.scheme) && !buffer_string_is_empty(&r->uri.authority)) { 425 chunkqueue_append_mem(cq, CONST_BUF_LEN(&r->uri.scheme)); 426 chunkqueue_append_mem(cq, CONST_STR_LEN("://")); 427 chunkqueue_append_mem(cq, CONST_BUF_LEN(&r->uri.authority)); 428 chunkqueue_append_mem(cq, CONST_BUF_LEN(&r->target)); 429 if (!buffer_string_is_empty(&r->uri.query)) { 430 chunkqueue_append_mem(cq, CONST_STR_LEN("?")); 431 chunkqueue_append_mem(cq, CONST_BUF_LEN(&r->uri.query)); 432 } 433 } 434 break; 435 } 436 case SSI_ECHO_SCRIPT_URL: { 437 chunkqueue_append_mem(cq, CONST_BUF_LEN(&r->target)); 438 if (!buffer_string_is_empty(&r->uri.query)) { 439 chunkqueue_append_mem(cq, CONST_STR_LEN("?")); 440 chunkqueue_append_mem(cq, CONST_BUF_LEN(&r->uri.query)); 441 } 442 break; 443 } 444 default: { 445 const data_string *ds; 446 /* check if it is a cgi-var or a ssi-var */ 447 448 if (NULL != (ds = (const data_string *)array_get_element_klen(p->ssi_cgi_env, var_val, strlen(var_val))) || 449 NULL != (ds = (const data_string *)array_get_element_klen(p->ssi_vars, var_val, strlen(var_val)))) { 450 chunkqueue_append_mem(cq, CONST_BUF_LEN(&ds->value)); 451 } else { 452 chunkqueue_append_mem(cq, CONST_STR_LEN("(none)")); 453 } 454 455 break; 456 } 457 } 458 break; 459 } 460 case SSI_INCLUDE: 461 case SSI_FLASTMOD: 462 case SSI_FSIZE: { 463 const char * file_path = NULL, *virt_path = NULL; 464 struct stat stb; 465 466 for (i = 2; i < n; i += 2) { 467 if (0 == strcmp(l[i], "file")) { 468 file_path = l[i+1]; 469 } else if (0 == strcmp(l[i], "virtual")) { 470 virt_path = l[i+1]; 471 } else { 472 log_error(r->conf.errh, __FILE__, __LINE__, 473 "ssi: unknown attribute for %s %s", l[1], l[i]); 474 } 475 } 476 477 if (!file_path && !virt_path) { 478 log_error(r->conf.errh, __FILE__, __LINE__, 479 "ssi: %s file or virtual is missing", l[1]); 480 break; 481 } 482 483 if (file_path && virt_path) { 484 log_error(r->conf.errh, __FILE__, __LINE__, 485 "ssi: %s only one of file and virtual is allowed here", l[1]); 486 break; 487 } 488 489 490 if (p->if_is_false) break; 491 492 tb = r->tmp_buf; 493 494 if (file_path) { 495 /* current doc-root */ 496 buffer_copy_string(tb, file_path); 497 buffer_urldecode_path(tb); 498 if (!buffer_is_valid_UTF8(tb)) { 499 log_error(r->conf.errh, __FILE__, __LINE__, 500 "SSI invalid UTF-8 after url-decode: %s", tb->ptr); 501 break; 502 } 503 buffer_path_simplify(tb, tb); 504 char *sl = strrchr(r->physical.path.ptr, '/'); 505 if (NULL == sl) break; /*(not expected)*/ 506 buffer_copy_path_len2(p->stat_fn, 507 r->physical.path.ptr, 508 sl - r->physical.path.ptr + 1, 509 CONST_BUF_LEN(tb)); 510 } else { 511 /* virtual */ 512 513 buffer_clear(tb); 514 if (virt_path[0] != '/') { 515 /* there is always a / */ 516 const char * const sl = strrchr(r->uri.path.ptr, '/'); 517 buffer_copy_string_len(tb, r->uri.path.ptr, sl - r->uri.path.ptr + 1); 518 } 519 buffer_append_string(tb, virt_path); 520 521 buffer_urldecode_path(tb); 522 if (!buffer_is_valid_UTF8(tb)) { 523 log_error(r->conf.errh, __FILE__, __LINE__, 524 "SSI invalid UTF-8 after url-decode: %s", tb->ptr); 525 break; 526 } 527 buffer_path_simplify(tb, tb); 528 529 /* we have an uri */ 530 531 /* Destination physical path (similar to code in mod_webdav.c) 532 * src r->physical.path might have been remapped with mod_alias, mod_userdir. 533 * (but neither modifies r->physical.rel_path) 534 * Find matching prefix to support relative paths to current physical path. 535 * Aliasing of paths underneath current r->physical.basedir might not work. 536 * Likewise, mod_rewrite URL rewriting might thwart this comparison. 537 * Use mod_redirect instead of mod_alias to remap paths *under* this basedir. 538 * Use mod_redirect instead of mod_rewrite on *any* parts of path to basedir. 539 * (Related, use mod_auth to protect this basedir, but avoid attempting to 540 * use mod_auth on paths underneath this basedir, as target path is not 541 * validated with mod_auth) 542 */ 543 544 /* find matching URI prefix 545 * check if remaining r->physical.rel_path matches suffix 546 * of r->physical.basedir so that we can use it to 547 * remap Destination physical path */ 548 { 549 const char *sep, *sep2; 550 sep = r->uri.path.ptr; 551 sep2 = tb->ptr; 552 for (i = 0; sep[i] && sep[i] == sep2[i]; ++i) ; 553 while (i != 0 && sep[--i] != '/') ; /* find matching directory path */ 554 } 555 if (r->conf.force_lowercase_filenames) { 556 buffer_to_lower(tb); 557 } 558 uint32_t remain = buffer_string_length(&r->uri.path) - i; 559 if (!r->conf.force_lowercase_filenames 560 ? buffer_is_equal_right_len(&r->physical.path, &r->physical.rel_path, remain) 561 :(buffer_string_length(&r->physical.path) >= remain 562 && buffer_eq_icase_ssn(r->physical.path.ptr+buffer_string_length(&r->physical.path)-remain, r->physical.rel_path.ptr+i, remain))) { 563 buffer_copy_path_len2(p->stat_fn, 564 r->physical.path.ptr, 565 buffer_string_length(&r->physical.path) 566 - remain, 567 tb->ptr+i, 568 buffer_string_length(tb)-i); 569 } else { 570 /* unable to perform physical path remap here; 571 * assume doc_root/rel_path and no remapping */ 572 buffer_copy_path_len2(p->stat_fn, 573 CONST_BUF_LEN(&r->physical.doc_root), 574 CONST_BUF_LEN(tb)); 575 } 576 } 577 578 if (!r->conf.follow_symlink 579 && 0 != stat_cache_path_contains_symlink(p->stat_fn, r->conf.errh)) { 580 break; 581 } 582 583 int fd = stat_cache_open_rdonly_fstat(p->stat_fn, &stb, r->conf.follow_symlink); 584 if (fd >= 0) { 585 time_t t = stb.st_mtime; 586 587 switch (ssicmd) { 588 case SSI_FSIZE: 589 buffer_clear(tb); 590 if (p->sizefmt) { 591 int j = 0; 592 const char *abr[] = { " B", " kB", " MB", " GB", " TB", NULL }; 593 594 off_t s = stb.st_size; 595 596 for (j = 0; s > 1024 && abr[j+1]; s /= 1024, j++); 597 598 buffer_append_int(tb, s); 599 buffer_append_string_len(tb, abr[j], j ? 3 : 2); 600 } else { 601 buffer_append_int(tb, stb.st_size); 602 } 603 chunkqueue_append_mem(cq, CONST_BUF_LEN(tb)); 604 break; 605 case SSI_FLASTMOD: 606 buffer_clear(tb); 607 mod_ssi_timefmt(tb, p->timefmt, t, 1); 608 chunkqueue_append_mem(cq, CONST_BUF_LEN(tb)); 609 break; 610 case SSI_INCLUDE: 611 /* Keep the newest mtime of included files */ 612 if (stb.st_mtime > include_file_last_mtime) 613 include_file_last_mtime = stb.st_mtime; 614 615 if (file_path || 0 == p->conf.ssi_recursion_max) { 616 /* don't process if #include file="..." is used */ 617 chunkqueue_append_file_fd(cq, p->stat_fn, fd, 0, stb.st_size); 618 fd = -1; 619 } else { 620 buffer upsave, ppsave, prpsave; 621 622 /* only allow predefined recursion depth */ 623 if (p->ssi_recursion_depth >= p->conf.ssi_recursion_max) { 624 chunkqueue_append_mem(cq, CONST_STR_LEN("(error: include directives recurse deeper than pre-defined ssi.recursion-max)")); 625 break; 626 } 627 628 /* prevents simple infinite loop */ 629 if (buffer_is_equal(&r->physical.path, p->stat_fn)) { 630 chunkqueue_append_mem(cq, CONST_STR_LEN("(error: include directives create an infinite loop)")); 631 break; 632 } 633 634 /* save and restore r->physical.path, r->physical.rel_path, and r->uri.path around include 635 * 636 * tb contains url-decoded, path-simplified, and lowercased (if r->conf.force_lowercase) uri path of target. 637 * r->uri.path and r->physical.rel_path are set to the same since we only operate on filenames here, 638 * not full re-run of all modules for subrequest */ 639 upsave = r->uri.path; 640 ppsave = r->physical.path; 641 prpsave = r->physical.rel_path; 642 643 r->physical.path = *p->stat_fn; 644 memset(p->stat_fn, 0, sizeof(buffer)); 645 646 memset(&r->uri.path, 0, sizeof(buffer)); 647 buffer_copy_buffer(&r->uri.path, tb); 648 r->physical.rel_path = r->uri.path; 649 650 close(fd); 651 fd = -1; 652 653 /*(ignore return value; muddle along as best we can if error occurs)*/ 654 ++p->ssi_recursion_depth; 655 mod_ssi_process_file(r, p, &stb); 656 --p->ssi_recursion_depth; 657 658 free(r->uri.path.ptr); 659 r->uri.path = upsave; 660 r->physical.rel_path = prpsave; 661 662 free(p->stat_fn->ptr); 663 *p->stat_fn = r->physical.path; 664 r->physical.path = ppsave; 665 } 666 667 break; 668 } 669 670 if (fd >= 0) close(fd); 671 } else { 672 log_perror(r->conf.errh, __FILE__, __LINE__, 673 "ssi: stating %s failed", p->stat_fn->ptr); 674 } 675 break; 676 } 677 case SSI_SET: { 678 const char *key = NULL, *val = NULL; 679 for (i = 2; i < n; i += 2) { 680 if (0 == strcmp(l[i], "var")) { 681 key = l[i+1]; 682 } else if (0 == strcmp(l[i], "value")) { 683 val = l[i+1]; 684 } else { 685 log_error(r->conf.errh, __FILE__, __LINE__, 686 "ssi: unknown attribute for %s %s", l[1], l[i]); 687 } 688 } 689 690 if (p->if_is_false) break; 691 692 if (key && val) { 693 array_set_key_value(p->ssi_vars, key, strlen(key), val, strlen(val)); 694 } else if (key || val) { 695 log_error(r->conf.errh, __FILE__, __LINE__, 696 "ssi: var and value have to be set in <!--#set %s=%s -->", l[1], l[2]); 697 } else { 698 log_error(r->conf.errh, __FILE__, __LINE__, 699 "ssi: var and value have to be set in <!--#set var=... value=... -->"); 700 } 701 break; 702 } 703 case SSI_CONFIG: 704 if (p->if_is_false) break; 705 706 for (i = 2; i < n; i += 2) { 707 if (0 == strcmp(l[i], "timefmt")) { 708 buffer_copy_string(p->timefmt, l[i+1]); 709 } else if (0 == strcmp(l[i], "sizefmt")) { 710 if (0 == strcmp(l[i+1], "abbrev")) { 711 p->sizefmt = 1; 712 } else if (0 == strcmp(l[i+1], "bytes")) { 713 p->sizefmt = 0; 714 } else { 715 log_error(r->conf.errh, __FILE__, __LINE__, 716 "ssi: unknown value for attribute '%s' for %s %s", 717 l[i], l[1], l[i+1]); 718 } 719 } else { 720 log_error(r->conf.errh, __FILE__, __LINE__, 721 "ssi: unknown attribute for %s %s", l[1], l[i]); 722 } 723 } 724 break; 725 case SSI_PRINTENV: 726 if (p->if_is_false) break; 727 728 tb = r->tmp_buf; 729 buffer_clear(tb); 730 for (i = 0; i < p->ssi_vars->used; i++) { 731 data_string *ds = (data_string *)p->ssi_vars->sorted[i]; 732 733 buffer_append_str2(tb, CONST_BUF_LEN(&ds->key), CONST_STR_LEN("=")); 734 buffer_append_string_encoded(tb, CONST_BUF_LEN(&ds->value), ENCODING_MINIMAL_XML); 735 buffer_append_string_len(tb, CONST_STR_LEN("\n")); 736 } 737 for (i = 0; i < p->ssi_cgi_env->used; i++) { 738 data_string *ds = (data_string *)p->ssi_cgi_env->sorted[i]; 739 740 buffer_append_str2(tb, CONST_BUF_LEN(&ds->key), CONST_STR_LEN("=")); 741 buffer_append_string_encoded(tb, CONST_BUF_LEN(&ds->value), ENCODING_MINIMAL_XML); 742 buffer_append_string_len(tb, CONST_STR_LEN("\n")); 743 } 744 chunkqueue_append_mem(cq, CONST_BUF_LEN(tb)); 745 break; 746 case SSI_EXEC: { 747 const char *cmd = NULL; 748 pid_t pid; 749 chunk *c; 750 char *args[4]; 751 log_error_st *errh = p->errh; 752 753 if (!p->conf.ssi_exec) { /* <!--#exec ... --> disabled by config */ 754 break; 755 } 756 757 for (i = 2; i < n; i += 2) { 758 if (0 == strcmp(l[i], "cmd")) { 759 cmd = l[i+1]; 760 } else { 761 log_error(errh, __FILE__, __LINE__, 762 "ssi: unknown attribute for %s %s", l[1], l[i]); 763 } 764 } 765 766 if (p->if_is_false) break; 767 768 /* 769 * as exec is assumed evil it is implemented synchronously 770 */ 771 772 if (!cmd) break; 773 774 /* send cmd output to a temporary file */ 775 if (0 != chunkqueue_append_mem_to_tempfile(cq, "", 0, errh)) break; 776 c = cq->last; 777 778 *(const char **)&args[0] = "/bin/sh"; 779 *(const char **)&args[1] = "-c"; 780 *(const char **)&args[2] = cmd; 781 args[3] = NULL; 782 783 /*(expects STDIN_FILENO open to /dev/null)*/ 784 int serrh_fd = r->conf.serrh ? r->conf.serrh->errorlog_fd : -1; 785 pid = fdevent_fork_execve(args[0], args, NULL, -1, c->file.fd, serrh_fd, -1); 786 if (-1 == pid) { 787 log_perror(errh, __FILE__, __LINE__, "spawning exec failed: %s", cmd); 788 } else { 789 struct stat stb; 790 int status = 0; 791 792 /* wait for the client to end */ 793 /* NOTE: synchronous; blocks entire lighttpd server */ 794 795 /* 796 * OpenBSD and Solaris send a EINTR on SIGCHILD even if we ignore it 797 */ 798 if (fdevent_waitpid(pid, &status, 0) < 0) { 799 log_perror(errh, __FILE__, __LINE__, "waitpid failed"); 800 break; 801 } 802 if (!WIFEXITED(status)) { 803 log_error(errh, __FILE__, __LINE__, "process exited abnormally: %s", cmd); 804 } 805 if (0 == fstat(c->file.fd, &stb)) { 806 chunkqueue_update_file(cq, c, stb.st_size); 807 } 808 } 809 810 break; 811 } 812 case SSI_IF: { 813 const char *expr = NULL; 814 815 for (i = 2; i < n; i += 2) { 816 if (0 == strcmp(l[i], "expr")) { 817 expr = l[i+1]; 818 } else { 819 log_error(r->conf.errh, __FILE__, __LINE__, 820 "ssi: unknown attribute for %s %s", l[1], l[i]); 821 } 822 } 823 824 if (!expr) { 825 log_error(r->conf.errh, __FILE__, __LINE__, 826 "ssi: %s expr missing", l[1]); 827 break; 828 } 829 830 if ((!p->if_is_false) && 831 ((p->if_is_false_level == 0) || 832 (p->if_level < p->if_is_false_level))) { 833 switch (ssi_eval_expr(p, expr)) { 834 case -1: 835 case 0: 836 p->if_is_false = 1; 837 p->if_is_false_level = p->if_level; 838 break; 839 case 1: 840 p->if_is_false = 0; 841 break; 842 } 843 } 844 845 p->if_level++; 846 847 break; 848 } 849 case SSI_ELSE: 850 p->if_level--; 851 852 if (p->if_is_false) { 853 if ((p->if_level == p->if_is_false_level) && 854 (p->if_is_false_endif == 0)) { 855 p->if_is_false = 0; 856 } 857 } else { 858 p->if_is_false = 1; 859 860 p->if_is_false_level = p->if_level; 861 } 862 p->if_level++; 863 864 break; 865 case SSI_ELIF: { 866 const char *expr = NULL; 867 for (i = 2; i < n; i += 2) { 868 if (0 == strcmp(l[i], "expr")) { 869 expr = l[i+1]; 870 } else { 871 log_error(r->conf.errh, __FILE__, __LINE__, 872 "ssi: unknown attribute for %s %s", l[1], l[i]); 873 } 874 } 875 876 if (!expr) { 877 log_error(r->conf.errh, __FILE__, __LINE__, 878 "ssi: %s expr missing", l[1]); 879 break; 880 } 881 882 p->if_level--; 883 884 if (p->if_level == p->if_is_false_level) { 885 if ((p->if_is_false) && 886 (p->if_is_false_endif == 0)) { 887 switch (ssi_eval_expr(p, expr)) { 888 case -1: 889 case 0: 890 p->if_is_false = 1; 891 p->if_is_false_level = p->if_level; 892 break; 893 case 1: 894 p->if_is_false = 0; 895 break; 896 } 897 } else { 898 p->if_is_false = 1; 899 p->if_is_false_level = p->if_level; 900 p->if_is_false_endif = 1; 901 } 902 } 903 904 p->if_level++; 905 906 break; 907 } 908 case SSI_ENDIF: 909 p->if_level--; 910 911 if (p->if_level == p->if_is_false_level) { 912 p->if_is_false = 0; 913 p->if_is_false_endif = 0; 914 } 915 916 break; 917 case SSI_COMMENT: 918 break; 919 default: 920 log_error(r->conf.errh, __FILE__, __LINE__, 921 "ssi: unknown ssi-command: %s", l[1]); 922 break; 923 } 924 925 return 0; 926 927 } 928 929 __attribute_pure__ 930 static int mod_ssi_parse_ssi_stmt_value(const unsigned char * const s, const int len) { 931 int n; 932 const int c = (s[0] == '"' ? '"' : s[0] == '\'' ? '\'' : 0); 933 if (0 != c) { 934 for (n = 1; n < len; ++n) { 935 if (s[n] == c) return n+1; 936 if (s[n] == '\\') { 937 if (n+1 == len) return 0; /* invalid */ 938 ++n; 939 } 940 } 941 return 0; /* invalid */ 942 } else { 943 for (n = 0; n < len; ++n) { 944 if (isspace(s[n])) return n; 945 if (s[n] == '\\') { 946 if (n+1 == len) return 0; /* invalid */ 947 ++n; 948 } 949 } 950 return n; 951 } 952 } 953 954 static int mod_ssi_parse_ssi_stmt_offlen(int o[10], const unsigned char * const s, const int len) { 955 956 /** 957 * <!--#element attribute=value attribute=value ... --> 958 */ 959 960 /* s must begin "<!--#" and must end with "-->" */ 961 int n = 5; 962 o[0] = n; 963 for (; light_isalpha(s[n]); ++n) ; /*(n = 5 to begin after "<!--#")*/ 964 o[1] = n - o[0]; 965 if (0 == o[1]) return -1; /* empty token */ 966 967 if (n+3 == len) return 2; /* token only; no params */ 968 if (!isspace(s[n])) return -1; 969 do { ++n; } while (isspace(s[n])); /* string ends "-->", so n < len */ 970 if (n+3 == len) return 2; /* token only; no params */ 971 972 o[2] = n; 973 for (; light_isalpha(s[n]); ++n) ; 974 o[3] = n - o[2]; 975 if (0 == o[3] || s[n++] != '=') return -1; 976 977 o[4] = n; 978 o[5] = mod_ssi_parse_ssi_stmt_value(s+n, len-n-3); 979 if (0 == o[5]) return -1; /* empty or invalid token */ 980 n += o[5]; 981 982 if (n+3 == len) return 6; /* token and one param */ 983 if (!isspace(s[n])) return -1; 984 do { ++n; } while (isspace(s[n])); /* string ends "-->", so n < len */ 985 if (n+3 == len) return 6; /* token and one param */ 986 987 o[6] = n; 988 for (; light_isalpha(s[n]); ++n) ; 989 o[7] = n - o[6]; 990 if (0 == o[7] || s[n++] != '=') return -1; 991 992 o[8] = n; 993 o[9] = mod_ssi_parse_ssi_stmt_value(s+n, len-n-3); 994 if (0 == o[9]) return -1; /* empty or invalid token */ 995 n += o[9]; 996 997 if (n+3 == len) return 10; /* token and two params */ 998 if (!isspace(s[n])) return -1; 999 do { ++n; } while (isspace(s[n])); /* string ends "-->", so n < len */ 1000 if (n+3 == len) return 10; /* token and two params */ 1001 return -1; 1002 } 1003 1004 static void mod_ssi_parse_ssi_stmt(request_st * const r, handler_ctx * const p, char * const s, int len, struct stat * const st) { 1005 1006 /** 1007 * <!--#element attribute=value attribute=value ... --> 1008 */ 1009 1010 int o[10]; 1011 int m; 1012 const int n = mod_ssi_parse_ssi_stmt_offlen(o, (unsigned char *)s, len); 1013 char *l[6] = { s, NULL, NULL, NULL, NULL, NULL }; 1014 if (-1 == n) { 1015 /* ignore <!--#comment ... --> */ 1016 if (len >= 16 1017 && 0 == memcmp(s+5, "comment", sizeof("comment")-1) 1018 && (s[12] == ' ' || s[12] == '\t')) 1019 return; 1020 /* XXX: perhaps emit error comment instead of invalid <!--#...--> code to client */ 1021 chunkqueue_append_mem(&p->wq, s, len); /* append stmt as-is */ 1022 return; 1023 } 1024 1025 #if 0 1026 /* dup s and then modify s */ 1027 /*(l[0] is no longer used; was previously used in only one place for error reporting)*/ 1028 l[0] = malloc((size_t)(len+1)); 1029 memcpy(l[0], s, (size_t)len); 1030 (l[0])[len] = '\0'; 1031 #endif 1032 1033 /* modify s in-place to split string into arg tokens */ 1034 for (m = 0; m < n; m += 2) { 1035 char *ptr = s+o[m]; 1036 switch (*ptr) { 1037 case '"': 1038 case '\'': (++ptr)[o[m+1]-2] = '\0'; break; 1039 default: ptr[o[m+1]] = '\0'; break; 1040 } 1041 l[1+(m>>1)] = ptr; 1042 if (m == 4 || m == 8) { 1043 /* XXX: removing '\\' escapes from param value would be 1044 * the right thing to do, but would potentially change 1045 * current behavior, e.g. <!--#exec cmd=... --> */ 1046 } 1047 } 1048 1049 process_ssi_stmt(r, p, (const char **)l, 1+(n>>1), st); 1050 1051 #if 0 1052 free(l[0]); 1053 #endif 1054 } 1055 1056 static int mod_ssi_stmt_len(const char *s, const int len) { 1057 /* s must begin "<!--#" */ 1058 int n, sq = 0, dq = 0, bs = 0; 1059 for (n = 5; n < len; ++n) { /*(n = 5 to begin after "<!--#")*/ 1060 switch (s[n]) { 1061 default: 1062 break; 1063 case '-': 1064 if (!sq && !dq && n+2 < len && s[n+1] == '-' && s[n+2] == '>') return n+3; /* found end of stmt */ 1065 break; 1066 case '"': 1067 if (!sq && (!dq || !bs)) dq = !dq; 1068 break; 1069 case '\'': 1070 if (!dq && (!sq || !bs)) sq = !sq; 1071 break; 1072 case '\\': 1073 if (sq || dq) bs = !bs; 1074 break; 1075 } 1076 } 1077 return 0; /* incomplete directive "<!--#...-->" */ 1078 } 1079 1080 static void mod_ssi_read_fd(request_st * const r, handler_ctx * const p, struct stat * const st, int fd) { 1081 ssize_t rd; 1082 size_t offset, pretag; 1083 /* allocate to reduce chance of stack exhaustion upon deep recursion */ 1084 buffer * const b = chunk_buffer_acquire(); 1085 chunkqueue * const cq = &p->wq; 1086 const size_t bufsz = 8192; 1087 chunk_buffer_prepare_append(b, bufsz-1); 1088 char * const buf = b->ptr; 1089 1090 offset = 0; 1091 pretag = 0; 1092 while (0 < (rd = read(fd, buf+offset, bufsz-offset))) { 1093 char *s; 1094 size_t prelen = 0, len; 1095 offset += (size_t)rd; 1096 for (; (s = memchr(buf+prelen, '<', offset-prelen)); ++prelen) { 1097 prelen = s - buf; 1098 if (prelen + 5 <= offset) { /*("<!--#" is 5 chars)*/ 1099 if (0 != memcmp(s+1, CONST_STR_LEN("!--#"))) continue; /* loop to loop for next '<' */ 1100 1101 if (prelen - pretag && !p->if_is_false) { 1102 chunkqueue_append_mem(cq, buf+pretag, prelen-pretag); 1103 } 1104 1105 len = mod_ssi_stmt_len(buf+prelen, offset-prelen); 1106 if (len) { /* num of chars to be consumed */ 1107 mod_ssi_parse_ssi_stmt(r, p, buf+prelen, len, st); 1108 prelen += (len - 1); /* offset to '>' at end of SSI directive; incremented at top of loop */ 1109 pretag = prelen + 1; 1110 if (pretag == offset) { 1111 offset = pretag = 0; 1112 break; 1113 } 1114 } else if (0 == prelen && offset == bufsz) { /*(full buf)*/ 1115 /* SSI statement is way too long 1116 * NOTE: skipping this buf will expose *the rest* of this SSI statement */ 1117 chunkqueue_append_mem(cq, CONST_STR_LEN("<!-- [an error occurred: directive too long] ")); 1118 /* check if buf ends with "-" or "--" which might be part of "-->" 1119 * (buf contains at least 5 chars for "<!--#") */ 1120 if (buf[offset-2] == '-' && buf[offset-1] == '-') { 1121 chunkqueue_append_mem(cq, CONST_STR_LEN("--")); 1122 } else if (buf[offset-1] == '-') { 1123 chunkqueue_append_mem(cq, CONST_STR_LEN("-")); 1124 } 1125 offset = pretag = 0; 1126 break; 1127 } else { /* incomplete directive "<!--#...-->" */ 1128 memmove(buf, buf+prelen, (offset -= prelen)); 1129 pretag = 0; 1130 break; 1131 } 1132 } else if (prelen + 1 == offset || 0 == memcmp(s+1, "!--", offset - prelen - 1)) { 1133 if (prelen - pretag && !p->if_is_false) { 1134 chunkqueue_append_mem(cq, buf+pretag, prelen-pretag); 1135 } 1136 memcpy(buf, buf+prelen, (offset -= prelen)); 1137 pretag = 0; 1138 break; 1139 } 1140 /* loop to look for next '<' */ 1141 } 1142 if (offset == bufsz) { 1143 if (!p->if_is_false) { 1144 chunkqueue_append_mem(cq, buf+pretag, offset-pretag); 1145 } 1146 offset = pretag = 0; 1147 } 1148 /* flush intermediate cq to r->write_queue (and possibly to 1149 * temporary file) if last MEM_CHUNK has less than 1k-1 avail 1150 * (reduce occurrence of copying to reallocate larger chunk) */ 1151 if (cq->last && cq->last->type == MEM_CHUNK 1152 && buffer_string_space(cq->last->mem) < 1023) 1153 http_chunk_transfer_cqlen(r, cq, chunkqueue_length(cq)); 1154 } 1155 1156 if (0 != rd) { 1157 log_perror(r->conf.errh, __FILE__, __LINE__, 1158 "read(): %s", r->physical.path.ptr); 1159 } 1160 1161 if (offset - pretag) { 1162 /* copy remaining data in buf */ 1163 if (!p->if_is_false) { 1164 chunkqueue_append_mem(cq, buf+pretag, offset-pretag); 1165 } 1166 } 1167 1168 chunk_buffer_release(b); 1169 http_chunk_transfer_cqlen(r, cq, chunkqueue_length(cq)); 1170 } 1171 1172 1173 static int mod_ssi_process_file(request_st * const r, handler_ctx * const p, struct stat * const st) { 1174 int fd = stat_cache_open_rdonly_fstat(&r->physical.path, st, r->conf.follow_symlink); 1175 if (-1 == fd) { 1176 log_perror(r->conf.errh, __FILE__, __LINE__, 1177 "open(): %s", r->physical.path.ptr); 1178 return -1; 1179 } 1180 1181 mod_ssi_read_fd(r, p, st, fd); 1182 1183 close(fd); 1184 return 0; 1185 } 1186 1187 1188 static int mod_ssi_handle_request(request_st * const r, handler_ctx * const p) { 1189 struct stat st; 1190 1191 /* get a stream to the file */ 1192 1193 buffer_clear(p->timefmt); 1194 array_reset_data_strings(p->ssi_vars); 1195 array_reset_data_strings(p->ssi_cgi_env); 1196 build_ssi_cgi_vars(r, p); 1197 1198 /* Reset the modified time of included files */ 1199 include_file_last_mtime = 0; 1200 1201 if (mod_ssi_process_file(r, p, &st)) return -1; 1202 1203 r->resp_body_started = 1; 1204 r->resp_body_finished = 1; 1205 1206 if (buffer_string_is_empty(p->conf.content_type)) { 1207 http_header_response_set(r, HTTP_HEADER_CONTENT_TYPE, CONST_STR_LEN("Content-Type"), CONST_STR_LEN("text/html")); 1208 } else { 1209 http_header_response_set(r, HTTP_HEADER_CONTENT_TYPE, CONST_STR_LEN("Content-Type"), CONST_BUF_LEN(p->conf.content_type)); 1210 } 1211 1212 if (p->conf.conditional_requests) { 1213 /* Generate "ETag" & "Last-Modified" headers */ 1214 1215 /* use most recently modified include file for ETag and Last-Modified */ 1216 if (st.st_mtime < include_file_last_mtime) 1217 st.st_mtime = include_file_last_mtime; 1218 1219 http_etag_create(&r->physical.etag, &st, r->conf.etag_flags); 1220 http_header_response_set(r, HTTP_HEADER_ETAG, CONST_STR_LEN("ETag"), CONST_BUF_LEN(&r->physical.etag)); 1221 1222 const buffer * const mtime = http_response_set_last_modified(r, st.st_mtime); 1223 if (HANDLER_FINISHED == http_response_handle_cachable(r, mtime, st.st_mtime)) { 1224 /* ok, the client already has our content, 1225 * no need to send it again */ 1226 1227 chunkqueue_reset(&r->write_queue); 1228 } 1229 } 1230 1231 /* Reset the modified time of included files */ 1232 include_file_last_mtime = 0; 1233 1234 return 0; 1235 } 1236 1237 URIHANDLER_FUNC(mod_ssi_physical_path) { 1238 plugin_data *p = p_d; 1239 1240 if (NULL != r->handler_module) return HANDLER_GO_ON; 1241 if (buffer_is_empty(&r->physical.path)) return HANDLER_GO_ON; 1242 1243 mod_ssi_patch_config(r, p); 1244 if (NULL == p->conf.ssi_extension) return HANDLER_GO_ON; 1245 1246 if (array_match_value_suffix(p->conf.ssi_extension, &r->physical.path)) { 1247 r->plugin_ctx[p->id] = handler_ctx_init(p, r->conf.errh); 1248 r->handler_module = p->self; 1249 } 1250 1251 return HANDLER_GO_ON; 1252 } 1253 1254 SUBREQUEST_FUNC(mod_ssi_handle_subrequest) { 1255 plugin_data *p = p_d; 1256 handler_ctx *hctx = r->plugin_ctx[p->id]; 1257 if (NULL == hctx) return HANDLER_GO_ON; 1258 /* 1259 * NOTE: if mod_ssi modified to use fdevents, HANDLER_WAIT_FOR_EVENT, 1260 * instead of blocking to completion, then hctx->timefmt, hctx->ssi_vars, 1261 * and hctx->ssi_cgi_env should be allocated and cleaned up per request. 1262 */ 1263 1264 /* handle ssi-request */ 1265 1266 if (mod_ssi_handle_request(r, hctx)) { 1267 /* on error */ 1268 r->http_status = 500; 1269 r->handler_module = NULL; 1270 } 1271 1272 return HANDLER_FINISHED; 1273 } 1274 1275 static handler_t mod_ssi_handle_request_reset(request_st * const r, void *p_d) { 1276 plugin_data *p = p_d; 1277 handler_ctx *hctx = r->plugin_ctx[p->id]; 1278 if (hctx) { 1279 handler_ctx_free(hctx); 1280 r->plugin_ctx[p->id] = NULL; 1281 } 1282 1283 return HANDLER_GO_ON; 1284 } 1285 1286 1287 int mod_ssi_plugin_init(plugin *p); 1288 int mod_ssi_plugin_init(plugin *p) { 1289 p->version = LIGHTTPD_VERSION_ID; 1290 p->name = "ssi"; 1291 1292 p->init = mod_ssi_init; 1293 p->handle_subrequest_start = mod_ssi_physical_path; 1294 p->handle_subrequest = mod_ssi_handle_subrequest; 1295 p->handle_request_reset = mod_ssi_handle_request_reset; 1296 p->set_defaults = mod_ssi_set_defaults; 1297 p->cleanup = mod_ssi_free; 1298 1299 return 0; 1300 } 1301