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