xref: /lighttpd1.4/src/mod_ssi.c (revision 3392e8fb)
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