use buffer_string_set_length() to truncate strings
[lighttpd.git] / src / mod_ssi.c
blob4dcfdd1ab97c56e2376eecd01621a4c69b76973c
1 #include "first.h"
3 #include "base.h"
4 #include "log.h"
5 #include "buffer.h"
7 #include "plugin.h"
8 #include "stream.h"
10 #include "response.h"
12 #include "mod_ssi.h"
14 #include "inet_ntop_cache.h"
16 #include "sys-socket.h"
18 #include <sys/types.h>
20 #include <ctype.h>
21 #include <stdlib.h>
22 #include <stdio.h>
23 #include <string.h>
24 #include <strings.h>
25 #include <errno.h>
26 #include <fcntl.h>
27 #include <time.h>
28 #include <unistd.h>
30 #ifdef HAVE_PWD_H
31 # include <pwd.h>
32 #endif
34 #ifdef HAVE_FORK
35 # include <sys/wait.h>
36 #endif
38 #ifdef HAVE_SYS_FILIO_H
39 # include <sys/filio.h>
40 #endif
42 #include "etag.h"
43 #include "version.h"
45 /* The newest modified time of included files for include statement */
46 static volatile time_t include_file_last_mtime = 0;
48 /* init the plugin data */
49 INIT_FUNC(mod_ssi_init) {
50 plugin_data *p;
52 p = calloc(1, sizeof(*p));
54 p->timefmt = buffer_init();
55 p->stat_fn = buffer_init();
57 p->ssi_vars = array_init();
58 p->ssi_cgi_env = array_init();
60 return p;
63 /* detroy the plugin data */
64 FREE_FUNC(mod_ssi_free) {
65 plugin_data *p = p_d;
66 UNUSED(srv);
68 if (!p) return HANDLER_GO_ON;
70 if (p->config_storage) {
71 size_t i;
72 for (i = 0; i < srv->config_context->used; i++) {
73 plugin_config *s = p->config_storage[i];
75 if (NULL == s) continue;
77 array_free(s->ssi_extension);
78 buffer_free(s->content_type);
80 free(s);
82 free(p->config_storage);
85 array_free(p->ssi_vars);
86 array_free(p->ssi_cgi_env);
87 buffer_free(p->timefmt);
88 buffer_free(p->stat_fn);
90 free(p);
92 return HANDLER_GO_ON;
95 /* handle plugin config and check values */
97 SETDEFAULTS_FUNC(mod_ssi_set_defaults) {
98 plugin_data *p = p_d;
99 size_t i = 0;
101 config_values_t cv[] = {
102 { "ssi.extension", NULL, T_CONFIG_ARRAY, T_CONFIG_SCOPE_CONNECTION }, /* 0 */
103 { "ssi.content-type", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 1 */
104 { "ssi.conditional-requests", NULL, T_CONFIG_BOOLEAN, T_CONFIG_SCOPE_CONNECTION }, /* 2 */
105 { "ssi.exec", NULL, T_CONFIG_BOOLEAN, T_CONFIG_SCOPE_CONNECTION }, /* 3 */
106 { NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET }
109 if (!p) return HANDLER_ERROR;
111 p->config_storage = calloc(1, srv->config_context->used * sizeof(plugin_config *));
113 for (i = 0; i < srv->config_context->used; i++) {
114 data_config const* config = (data_config const*)srv->config_context->data[i];
115 plugin_config *s;
117 s = calloc(1, sizeof(plugin_config));
118 s->ssi_extension = array_init();
119 s->content_type = buffer_init();
120 s->conditional_requests = 0;
121 s->ssi_exec = 1;
123 cv[0].destination = s->ssi_extension;
124 cv[1].destination = s->content_type;
125 cv[2].destination = &(s->conditional_requests);
126 cv[3].destination = &(s->ssi_exec);
128 p->config_storage[i] = s;
130 if (0 != config_insert_values_global(srv, config->value, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) {
131 return HANDLER_ERROR;
135 return HANDLER_GO_ON;
139 static int ssi_env_add(array *env, const char *key, const char *val) {
140 data_string *ds;
142 if (NULL == (ds = (data_string *)array_get_unused_element(env, TYPE_STRING))) {
143 ds = data_string_init();
145 buffer_copy_string(ds->key, key);
146 buffer_copy_string(ds->value, val);
148 array_insert_unique(env, (data_unset *)ds);
150 return 0;
155 * the next two functions are take from fcgi.c
159 static int ssi_env_add_request_headers(server *srv, connection *con, plugin_data *p) {
160 size_t i;
162 for (i = 0; i < con->request.headers->used; i++) {
163 data_string *ds;
165 ds = (data_string *)con->request.headers->data[i];
167 if (!buffer_is_empty(ds->value) && !buffer_is_empty(ds->key)) {
168 /* don't forward the Authorization: Header */
169 if (0 == strcasecmp(ds->key->ptr, "AUTHORIZATION")) {
170 continue;
173 buffer_copy_string_encoded_cgi_varnames(srv->tmp_buf, CONST_BUF_LEN(ds->key), 1);
175 ssi_env_add(p->ssi_cgi_env, srv->tmp_buf->ptr, ds->value->ptr);
179 for (i = 0; i < con->environment->used; i++) {
180 data_string *ds;
182 ds = (data_string *)con->environment->data[i];
184 if (!buffer_is_empty(ds->value) && !buffer_is_empty(ds->key)) {
185 buffer_copy_string_encoded_cgi_varnames(srv->tmp_buf, CONST_BUF_LEN(ds->key), 0);
187 ssi_env_add(p->ssi_cgi_env, srv->tmp_buf->ptr, ds->value->ptr);
191 return 0;
194 static int build_ssi_cgi_vars(server *srv, connection *con, plugin_data *p) {
195 char buf[LI_ITOSTRING_LENGTH];
197 server_socket *srv_sock = con->srv_socket;
199 #ifdef HAVE_IPV6
200 char b2[INET6_ADDRSTRLEN + 1];
201 #endif
203 #define CONST_STRING(x) \
206 array_reset(p->ssi_cgi_env);
208 ssi_env_add(p->ssi_cgi_env, CONST_STRING("SERVER_SOFTWARE"), PACKAGE_DESC);
209 ssi_env_add(p->ssi_cgi_env, CONST_STRING("SERVER_NAME"),
210 #ifdef HAVE_IPV6
211 inet_ntop(srv_sock->addr.plain.sa_family,
212 srv_sock->addr.plain.sa_family == AF_INET6 ?
213 (const void *) &(srv_sock->addr.ipv6.sin6_addr) :
214 (const void *) &(srv_sock->addr.ipv4.sin_addr),
215 b2, sizeof(b2)-1)
216 #else
217 inet_ntoa(srv_sock->addr.ipv4.sin_addr)
218 #endif
220 ssi_env_add(p->ssi_cgi_env, CONST_STRING("GATEWAY_INTERFACE"), "CGI/1.1");
222 li_utostrn(buf, sizeof(buf),
223 #ifdef HAVE_IPV6
224 ntohs(srv_sock->addr.plain.sa_family ? srv_sock->addr.ipv6.sin6_port : srv_sock->addr.ipv4.sin_port)
225 #else
226 ntohs(srv_sock->addr.ipv4.sin_port)
227 #endif
230 ssi_env_add(p->ssi_cgi_env, CONST_STRING("SERVER_PORT"), buf);
232 ssi_env_add(p->ssi_cgi_env, CONST_STRING("REMOTE_ADDR"),
233 inet_ntop_cache_get_ip(srv, &(con->dst_addr)));
235 if (con->request.content_length > 0) {
236 /* CGI-SPEC 6.1.2 and FastCGI spec 6.3 */
238 li_itostrn(buf, sizeof(buf), con->request.content_length);
239 ssi_env_add(p->ssi_cgi_env, CONST_STRING("CONTENT_LENGTH"), buf);
243 * SCRIPT_NAME, PATH_INFO and PATH_TRANSLATED according to
244 * http://cgi-spec.golux.com/draft-coar-cgi-v11-03-clean.html
245 * (6.1.14, 6.1.6, 6.1.7)
248 ssi_env_add(p->ssi_cgi_env, CONST_STRING("SCRIPT_NAME"), con->uri.path->ptr);
249 ssi_env_add(p->ssi_cgi_env, CONST_STRING("PATH_INFO"), "");
252 * SCRIPT_FILENAME and DOCUMENT_ROOT for php. The PHP manual
253 * http://www.php.net/manual/en/reserved.variables.php
254 * treatment of PATH_TRANSLATED is different from the one of CGI specs.
255 * TODO: this code should be checked against cgi.fix_pathinfo php
256 * parameter.
259 if (!buffer_string_is_empty(con->request.pathinfo)) {
260 ssi_env_add(p->ssi_cgi_env, CONST_STRING("PATH_INFO"), con->request.pathinfo->ptr);
263 ssi_env_add(p->ssi_cgi_env, CONST_STRING("SCRIPT_FILENAME"), con->physical.path->ptr);
264 ssi_env_add(p->ssi_cgi_env, CONST_STRING("DOCUMENT_ROOT"), con->physical.basedir->ptr);
266 ssi_env_add(p->ssi_cgi_env, CONST_STRING("REQUEST_URI"), con->request.uri->ptr);
268 if (!buffer_string_is_empty(con->uri.scheme)) {
269 ssi_env_add(p->ssi_cgi_env, CONST_STRING("REQUEST_SCHEME"), con->uri.scheme->ptr);
272 ssi_env_add(p->ssi_cgi_env, CONST_STRING("QUERY_STRING"), buffer_is_empty(con->uri.query) ? "" : con->uri.query->ptr);
273 ssi_env_add(p->ssi_cgi_env, CONST_STRING("REQUEST_METHOD"), get_http_method_name(con->request.http_method));
274 ssi_env_add(p->ssi_cgi_env, CONST_STRING("SERVER_PROTOCOL"), get_http_version_name(con->request.http_version));
275 /* set REDIRECT_STATUS for php compiled with --force-redirect
276 * (if REDIRECT_STATUS has not already been set by error handler) */
277 if (0 == con->error_handler_saved_status) {
278 ssi_env_add(p->ssi_cgi_env, CONST_STRING("REDIRECT_STATUS"), "200");
281 ssi_env_add_request_headers(srv, con, p);
283 return 0;
286 static int process_ssi_stmt(server *srv, connection *con, plugin_data *p, const char **l, size_t n, struct stat *st) {
289 * <!--#element attribute=value attribute=value ... -->
291 * config DONE
292 * errmsg -- missing
293 * sizefmt DONE
294 * timefmt DONE
295 * echo DONE
296 * var DONE
297 * encoding -- missing
298 * exec DONE
299 * cgi -- never
300 * cmd DONE
301 * fsize DONE
302 * file DONE
303 * virtual DONE
304 * flastmod DONE
305 * file DONE
306 * virtual DONE
307 * include DONE
308 * file DONE
309 * virtual DONE
310 * printenv DONE
311 * set DONE
312 * var DONE
313 * value DONE
315 * if DONE
316 * elif DONE
317 * else DONE
318 * endif DONE
321 * expressions
322 * AND, OR DONE
323 * comp DONE
324 * ${...} -- missing
325 * $... DONE
326 * '...' DONE
327 * ( ... ) DONE
331 * ** all DONE **
332 * DATE_GMT
333 * The current date in Greenwich Mean Time.
334 * DATE_LOCAL
335 * The current date in the local time zone.
336 * DOCUMENT_NAME
337 * The filename (excluding directories) of the document requested by the user.
338 * DOCUMENT_URI
339 * 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.
340 * LAST_MODIFIED
341 * The last modification date of the document requested by the user.
342 * USER_NAME
343 * Contains the owner of the file which included it.
347 size_t i, ssicmd = 0;
348 char buf[255];
349 buffer *b = NULL;
351 struct {
352 const char *var;
353 enum { SSI_UNSET, SSI_ECHO, SSI_FSIZE, SSI_INCLUDE, SSI_FLASTMOD,
354 SSI_CONFIG, SSI_PRINTENV, SSI_SET, SSI_IF, SSI_ELIF,
355 SSI_ELSE, SSI_ENDIF, SSI_EXEC } type;
356 } ssicmds[] = {
357 { "echo", SSI_ECHO },
358 { "include", SSI_INCLUDE },
359 { "flastmod", SSI_FLASTMOD },
360 { "fsize", SSI_FSIZE },
361 { "config", SSI_CONFIG },
362 { "printenv", SSI_PRINTENV },
363 { "set", SSI_SET },
364 { "if", SSI_IF },
365 { "elif", SSI_ELIF },
366 { "endif", SSI_ENDIF },
367 { "else", SSI_ELSE },
368 { "exec", SSI_EXEC },
370 { NULL, SSI_UNSET }
373 for (i = 0; ssicmds[i].var; i++) {
374 if (0 == strcmp(l[1], ssicmds[i].var)) {
375 ssicmd = ssicmds[i].type;
376 break;
380 switch(ssicmd) {
381 case SSI_ECHO: {
382 /* echo */
383 int var = 0;
384 /* int enc = 0; */
385 const char *var_val = NULL;
387 struct {
388 const char *var;
389 enum {
390 SSI_ECHO_UNSET,
391 SSI_ECHO_DATE_GMT,
392 SSI_ECHO_DATE_LOCAL,
393 SSI_ECHO_DOCUMENT_NAME,
394 SSI_ECHO_DOCUMENT_URI,
395 SSI_ECHO_LAST_MODIFIED,
396 SSI_ECHO_USER_NAME,
397 SSI_ECHO_SCRIPT_URI,
398 SSI_ECHO_SCRIPT_URL,
399 } type;
400 } echovars[] = {
401 { "DATE_GMT", SSI_ECHO_DATE_GMT },
402 { "DATE_LOCAL", SSI_ECHO_DATE_LOCAL },
403 { "DOCUMENT_NAME", SSI_ECHO_DOCUMENT_NAME },
404 { "DOCUMENT_URI", SSI_ECHO_DOCUMENT_URI },
405 { "LAST_MODIFIED", SSI_ECHO_LAST_MODIFIED },
406 { "USER_NAME", SSI_ECHO_USER_NAME },
407 { "SCRIPT_URI", SSI_ECHO_SCRIPT_URI },
408 { "SCRIPT_URL", SSI_ECHO_SCRIPT_URL },
410 { NULL, SSI_ECHO_UNSET }
414 struct {
415 const char *var;
416 enum { SSI_ENC_UNSET, SSI_ENC_URL, SSI_ENC_NONE, SSI_ENC_ENTITY } type;
417 } encvars[] = {
418 { "url", SSI_ENC_URL },
419 { "none", SSI_ENC_NONE },
420 { "entity", SSI_ENC_ENTITY },
422 { NULL, SSI_ENC_UNSET }
426 for (i = 2; i < n; i += 2) {
427 if (0 == strcmp(l[i], "var")) {
428 int j;
430 var_val = l[i+1];
432 for (j = 0; echovars[j].var; j++) {
433 if (0 == strcmp(l[i+1], echovars[j].var)) {
434 var = echovars[j].type;
435 break;
438 } else if (0 == strcmp(l[i], "encoding")) {
440 int j;
442 for (j = 0; encvars[j].var; j++) {
443 if (0 == strcmp(l[i+1], encvars[j].var)) {
444 enc = encvars[j].type;
445 break;
449 } else {
450 log_error_write(srv, __FILE__, __LINE__, "sss",
451 "ssi: unknown attribute for ",
452 l[1], l[i]);
456 if (p->if_is_false) break;
458 if (!var_val) {
459 log_error_write(srv, __FILE__, __LINE__, "sss",
460 "ssi: ",
461 l[1], "var is missing");
462 break;
465 switch(var) {
466 case SSI_ECHO_USER_NAME: {
467 struct passwd *pw;
469 b = buffer_init();
470 #ifdef HAVE_PWD_H
471 if (NULL == (pw = getpwuid(st->st_uid))) {
472 buffer_copy_int(b, st->st_uid);
473 } else {
474 buffer_copy_string(b, pw->pw_name);
476 #else
477 buffer_copy_int(b, st->st_uid);
478 #endif
479 chunkqueue_append_buffer(con->write_queue, b);
480 buffer_free(b);
481 break;
483 case SSI_ECHO_LAST_MODIFIED: {
484 time_t t = st->st_mtime;
486 if (0 == strftime(buf, sizeof(buf), p->timefmt->ptr, localtime(&t))) {
487 chunkqueue_append_mem(con->write_queue, CONST_STR_LEN("(none)"));
488 } else {
489 chunkqueue_append_mem(con->write_queue, buf, strlen(buf));
491 break;
493 case SSI_ECHO_DATE_LOCAL: {
494 time_t t = time(NULL);
496 if (0 == strftime(buf, sizeof(buf), p->timefmt->ptr, localtime(&t))) {
497 chunkqueue_append_mem(con->write_queue, CONST_STR_LEN("(none)"));
498 } else {
499 chunkqueue_append_mem(con->write_queue, buf, strlen(buf));
501 break;
503 case SSI_ECHO_DATE_GMT: {
504 time_t t = time(NULL);
506 if (0 == strftime(buf, sizeof(buf), p->timefmt->ptr, gmtime(&t))) {
507 chunkqueue_append_mem(con->write_queue, CONST_STR_LEN("(none)"));
508 } else {
509 chunkqueue_append_mem(con->write_queue, buf, strlen(buf));
511 break;
513 case SSI_ECHO_DOCUMENT_NAME: {
514 char *sl;
516 if (NULL == (sl = strrchr(con->physical.path->ptr, '/'))) {
517 chunkqueue_append_mem(con->write_queue, CONST_BUF_LEN(con->physical.path));
518 } else {
519 chunkqueue_append_mem(con->write_queue, sl + 1, strlen(sl + 1));
521 break;
523 case SSI_ECHO_DOCUMENT_URI: {
524 chunkqueue_append_mem(con->write_queue, CONST_BUF_LEN(con->uri.path));
525 break;
527 case SSI_ECHO_SCRIPT_URI: {
528 if (!buffer_string_is_empty(con->uri.scheme) && !buffer_string_is_empty(con->uri.authority)) {
529 chunkqueue_append_mem(con->write_queue, CONST_BUF_LEN(con->uri.scheme));
530 chunkqueue_append_mem(con->write_queue, CONST_STR_LEN("://"));
531 chunkqueue_append_mem(con->write_queue, CONST_BUF_LEN(con->uri.authority));
532 chunkqueue_append_mem(con->write_queue, CONST_BUF_LEN(con->request.uri));
533 if (!buffer_string_is_empty(con->uri.query)) {
534 chunkqueue_append_mem(con->write_queue, CONST_STR_LEN("?"));
535 chunkqueue_append_mem(con->write_queue, CONST_BUF_LEN(con->uri.query));
538 break;
540 case SSI_ECHO_SCRIPT_URL: {
541 chunkqueue_append_mem(con->write_queue, CONST_BUF_LEN(con->request.uri));
542 if (!buffer_string_is_empty(con->uri.query)) {
543 chunkqueue_append_mem(con->write_queue, CONST_STR_LEN("?"));
544 chunkqueue_append_mem(con->write_queue, CONST_BUF_LEN(con->uri.query));
546 break;
548 default: {
549 data_string *ds;
550 /* check if it is a cgi-var or a ssi-var */
552 if (NULL != (ds = (data_string *)array_get_element(p->ssi_cgi_env, var_val)) ||
553 NULL != (ds = (data_string *)array_get_element(p->ssi_vars, var_val))) {
554 chunkqueue_append_mem(con->write_queue, CONST_BUF_LEN(ds->value));
555 } else {
556 chunkqueue_append_mem(con->write_queue, CONST_STR_LEN("(none)"));
559 break;
562 break;
564 case SSI_INCLUDE:
565 case SSI_FLASTMOD:
566 case SSI_FSIZE: {
567 const char * file_path = NULL, *virt_path = NULL;
568 struct stat stb;
569 char *sl;
571 for (i = 2; i < n; i += 2) {
572 if (0 == strcmp(l[i], "file")) {
573 file_path = l[i+1];
574 } else if (0 == strcmp(l[i], "virtual")) {
575 virt_path = l[i+1];
576 } else {
577 log_error_write(srv, __FILE__, __LINE__, "sss",
578 "ssi: unknown attribute for ",
579 l[1], l[i]);
583 if (!file_path && !virt_path) {
584 log_error_write(srv, __FILE__, __LINE__, "sss",
585 "ssi: ",
586 l[1], "file or virtual are missing");
587 break;
590 if (file_path && virt_path) {
591 log_error_write(srv, __FILE__, __LINE__, "sss",
592 "ssi: ",
593 l[1], "only one of file and virtual is allowed here");
594 break;
598 if (p->if_is_false) break;
600 if (file_path) {
601 /* current doc-root */
602 if (NULL == (sl = strrchr(con->physical.path->ptr, '/'))) {
603 buffer_copy_string_len(p->stat_fn, CONST_STR_LEN("/"));
604 } else {
605 buffer_copy_string_len(p->stat_fn, con->physical.path->ptr, sl - con->physical.path->ptr + 1);
608 buffer_copy_string(srv->tmp_buf, file_path);
609 buffer_urldecode_path(srv->tmp_buf);
610 buffer_path_simplify(srv->tmp_buf, srv->tmp_buf);
611 buffer_append_string_buffer(p->stat_fn, srv->tmp_buf);
612 } else {
613 /* virtual */
614 size_t remain;
616 if (virt_path[0] == '/') {
617 buffer_copy_string(p->stat_fn, virt_path);
618 } else {
619 /* there is always a / */
620 sl = strrchr(con->uri.path->ptr, '/');
622 buffer_copy_string_len(p->stat_fn, con->uri.path->ptr, sl - con->uri.path->ptr + 1);
623 buffer_append_string(p->stat_fn, virt_path);
626 buffer_urldecode_path(p->stat_fn);
627 buffer_path_simplify(srv->tmp_buf, p->stat_fn);
629 /* we have an uri */
631 /* Destination physical path (similar to code in mod_webdav.c)
632 * src con->physical.path might have been remapped with mod_alias, mod_userdir.
633 * (but neither modifies con->physical.rel_path)
634 * Find matching prefix to support relative paths to current physical path.
635 * Aliasing of paths underneath current con->physical.basedir might not work.
636 * Likewise, mod_rewrite URL rewriting might thwart this comparison.
637 * Use mod_redirect instead of mod_alias to remap paths *under* this basedir.
638 * Use mod_redirect instead of mod_rewrite on *any* parts of path to basedir.
639 * (Related, use mod_auth to protect this basedir, but avoid attempting to
640 * use mod_auth on paths underneath this basedir, as target path is not
641 * validated with mod_auth)
644 /* find matching URI prefix
645 * check if remaining con->physical.rel_path matches suffix
646 * of con->physical.basedir so that we can use it to
647 * remap Destination physical path */
649 const char *sep, *sep2;
650 sep = con->uri.path->ptr;
651 sep2 = srv->tmp_buf->ptr;
652 for (i = 0; sep[i] && sep[i] == sep2[i]; ++i) ;
653 while (i != 0 && sep[--i] != '/') ; /* find matching directory path */
655 if (con->conf.force_lowercase_filenames) {
656 buffer_to_lower(srv->tmp_buf);
658 remain = buffer_string_length(con->uri.path) - i;
659 if (!con->conf.force_lowercase_filenames
660 ? buffer_is_equal_right_len(con->physical.path, con->physical.rel_path, remain)
661 :(buffer_string_length(con->physical.path) >= remain
662 && 0 == strncasecmp(con->physical.path->ptr+buffer_string_length(con->physical.path)-remain, con->physical.rel_path->ptr+i, remain))) {
663 buffer_copy_string_len(p->stat_fn, con->physical.path->ptr, buffer_string_length(con->physical.path)-remain);
664 buffer_append_string_len(p->stat_fn, srv->tmp_buf->ptr+i, buffer_string_length(srv->tmp_buf)-i);
665 } else {
666 /* unable to perform physical path remap here;
667 * assume doc_root/rel_path and no remapping */
668 buffer_copy_buffer(p->stat_fn, con->physical.doc_root);
669 buffer_append_string_buffer(p->stat_fn, srv->tmp_buf);
673 if (0 == stat(p->stat_fn->ptr, &stb)) {
674 time_t t = stb.st_mtime;
676 switch (ssicmd) {
677 case SSI_FSIZE:
678 b = buffer_init();
679 if (p->sizefmt) {
680 int j = 0;
681 const char *abr[] = { " B", " kB", " MB", " GB", " TB", NULL };
683 off_t s = stb.st_size;
685 for (j = 0; s > 1024 && abr[j+1]; s /= 1024, j++);
687 buffer_copy_int(b, s);
688 buffer_append_string(b, abr[j]);
689 } else {
690 buffer_copy_int(b, stb.st_size);
692 chunkqueue_append_buffer(con->write_queue, b);
693 buffer_free(b);
694 break;
695 case SSI_FLASTMOD:
696 if (0 == strftime(buf, sizeof(buf), p->timefmt->ptr, localtime(&t))) {
697 chunkqueue_append_mem(con->write_queue, CONST_STR_LEN("(none)"));
698 } else {
699 chunkqueue_append_mem(con->write_queue, buf, strlen(buf));
701 break;
702 case SSI_INCLUDE:
703 chunkqueue_append_file(con->write_queue, p->stat_fn, 0, stb.st_size);
705 /* Keep the newest mtime of included files */
706 if (stb.st_mtime > include_file_last_mtime)
707 include_file_last_mtime = stb.st_mtime;
709 break;
711 } else {
712 log_error_write(srv, __FILE__, __LINE__, "sbs",
713 "ssi: stating failed ",
714 p->stat_fn, strerror(errno));
716 break;
718 case SSI_SET: {
719 const char *key = NULL, *val = NULL;
720 for (i = 2; i < n; i += 2) {
721 if (0 == strcmp(l[i], "var")) {
722 key = l[i+1];
723 } else if (0 == strcmp(l[i], "value")) {
724 val = l[i+1];
725 } else {
726 log_error_write(srv, __FILE__, __LINE__, "sss",
727 "ssi: unknown attribute for ",
728 l[1], l[i]);
732 if (p->if_is_false) break;
734 if (key && val) {
735 data_string *ds;
737 if (NULL == (ds = (data_string *)array_get_unused_element(p->ssi_vars, TYPE_STRING))) {
738 ds = data_string_init();
740 buffer_copy_string(ds->key, key);
741 buffer_copy_string(ds->value, val);
743 array_insert_unique(p->ssi_vars, (data_unset *)ds);
744 } else if (key || val) {
745 log_error_write(srv, __FILE__, __LINE__, "sSSss",
746 "ssi: var and value have to be set in <!--#set", l[1], "=", l[2], "-->");
747 } else {
748 log_error_write(srv, __FILE__, __LINE__, "s",
749 "ssi: var and value have to be set in <!--#set var=... value=... -->");
751 break;
753 case SSI_CONFIG:
754 if (p->if_is_false) break;
756 for (i = 2; i < n; i += 2) {
757 if (0 == strcmp(l[i], "timefmt")) {
758 buffer_copy_string(p->timefmt, l[i+1]);
759 } else if (0 == strcmp(l[i], "sizefmt")) {
760 if (0 == strcmp(l[i+1], "abbrev")) {
761 p->sizefmt = 1;
762 } else if (0 == strcmp(l[i+1], "abbrev")) {
763 p->sizefmt = 0;
764 } else {
765 log_error_write(srv, __FILE__, __LINE__, "sssss",
766 "ssi: unknown value for attribute '",
767 l[i],
768 "' for ",
769 l[1], l[i+1]);
771 } else {
772 log_error_write(srv, __FILE__, __LINE__, "sss",
773 "ssi: unknown attribute for ",
774 l[1], l[i]);
777 break;
778 case SSI_PRINTENV:
779 if (p->if_is_false) break;
781 b = buffer_init();
782 for (i = 0; i < p->ssi_vars->used; i++) {
783 data_string *ds = (data_string *)p->ssi_vars->data[p->ssi_vars->sorted[i]];
785 buffer_append_string_buffer(b, ds->key);
786 buffer_append_string_len(b, CONST_STR_LEN("="));
787 buffer_append_string_encoded(b, CONST_BUF_LEN(ds->value), ENCODING_MINIMAL_XML);
788 buffer_append_string_len(b, CONST_STR_LEN("\n"));
790 for (i = 0; i < p->ssi_cgi_env->used; i++) {
791 data_string *ds = (data_string *)p->ssi_cgi_env->data[p->ssi_cgi_env->sorted[i]];
793 buffer_append_string_buffer(b, ds->key);
794 buffer_append_string_len(b, CONST_STR_LEN("="));
795 buffer_append_string_encoded(b, CONST_BUF_LEN(ds->value), ENCODING_MINIMAL_XML);
796 buffer_append_string_len(b, CONST_STR_LEN("\n"));
798 chunkqueue_append_buffer(con->write_queue, b);
799 buffer_free(b);
801 break;
802 case SSI_EXEC: {
803 const char *cmd = NULL;
804 pid_t pid;
805 int from_exec_fds[2];
807 if (!p->conf.ssi_exec) { /* <!--#exec ... --> disabled by config */
808 break;
811 for (i = 2; i < n; i += 2) {
812 if (0 == strcmp(l[i], "cmd")) {
813 cmd = l[i+1];
814 } else {
815 log_error_write(srv, __FILE__, __LINE__, "sss",
816 "ssi: unknown attribute for ",
817 l[1], l[i]);
821 if (p->if_is_false) break;
823 /* create a return pipe and send output to the html-page
825 * as exec is assumed evil it is implemented synchronously
828 if (!cmd) break;
829 #ifdef HAVE_FORK
830 if (pipe(from_exec_fds)) {
831 log_error_write(srv, __FILE__, __LINE__, "ss",
832 "pipe failed: ", strerror(errno));
833 return -1;
836 /* fork, execve */
837 switch (pid = fork()) {
838 case 0: {
839 /* move stdout to from_rrdtool_fd[1] */
840 close(STDOUT_FILENO);
841 dup2(from_exec_fds[1], STDOUT_FILENO);
842 close(from_exec_fds[1]);
843 /* not needed */
844 close(from_exec_fds[0]);
846 /* close stdin */
847 close(STDIN_FILENO);
849 execl("/bin/sh", "sh", "-c", cmd, (char *)NULL);
851 log_error_write(srv, __FILE__, __LINE__, "sss", "spawing exec failed:", strerror(errno), cmd);
853 /* */
854 SEGFAULT();
855 break;
857 case -1:
858 /* error */
859 log_error_write(srv, __FILE__, __LINE__, "ss", "fork failed:", strerror(errno));
860 break;
861 default: {
862 /* father */
863 int status;
864 ssize_t r;
865 int was_interrupted = 0;
867 close(from_exec_fds[1]);
869 /* wait for the client to end */
872 * OpenBSD and Solaris send a EINTR on SIGCHILD even if we ignore it
874 do {
875 if (-1 == waitpid(pid, &status, 0)) {
876 if (errno == EINTR) {
877 was_interrupted++;
878 } else {
879 was_interrupted = 0;
880 log_error_write(srv, __FILE__, __LINE__, "ss", "waitpid failed:", strerror(errno));
882 } else if (WIFEXITED(status)) {
883 int toread;
884 /* read everything from client and paste it into the output */
885 was_interrupted = 0;
887 while(1) {
888 if (ioctl(from_exec_fds[0], FIONREAD, &toread)) {
889 log_error_write(srv, __FILE__, __LINE__, "s",
890 "unexpected end-of-file (perhaps the ssi-exec process died)");
891 return -1;
894 if (toread > 0) {
895 char *mem;
896 size_t mem_len;
898 chunkqueue_get_memory(con->write_queue, &mem, &mem_len, 0, toread);
899 r = read(from_exec_fds[0], mem, mem_len);
900 chunkqueue_use_memory(con->write_queue, r > 0 ? r : 0);
902 if (r < 0) break; /* read failed */
903 } else {
904 break;
907 } else {
908 was_interrupted = 0;
909 log_error_write(srv, __FILE__, __LINE__, "s", "process exited abnormally");
911 } while (was_interrupted > 0 && was_interrupted < 4); /* if waitpid() gets interrupted, retry, but max 4 times */
913 close(from_exec_fds[0]);
915 break;
918 #else
920 return -1;
921 #endif
923 break;
925 case SSI_IF: {
926 const char *expr = NULL;
928 for (i = 2; i < n; i += 2) {
929 if (0 == strcmp(l[i], "expr")) {
930 expr = l[i+1];
931 } else {
932 log_error_write(srv, __FILE__, __LINE__, "sss",
933 "ssi: unknown attribute for ",
934 l[1], l[i]);
938 if (!expr) {
939 log_error_write(srv, __FILE__, __LINE__, "sss",
940 "ssi: ",
941 l[1], "expr missing");
942 break;
945 if ((!p->if_is_false) &&
946 ((p->if_is_false_level == 0) ||
947 (p->if_level < p->if_is_false_level))) {
948 switch (ssi_eval_expr(srv, con, p, expr)) {
949 case -1:
950 case 0:
951 p->if_is_false = 1;
952 p->if_is_false_level = p->if_level;
953 break;
954 case 1:
955 p->if_is_false = 0;
956 break;
960 p->if_level++;
962 break;
964 case SSI_ELSE:
965 p->if_level--;
967 if (p->if_is_false) {
968 if ((p->if_level == p->if_is_false_level) &&
969 (p->if_is_false_endif == 0)) {
970 p->if_is_false = 0;
972 } else {
973 p->if_is_false = 1;
975 p->if_is_false_level = p->if_level;
977 p->if_level++;
979 break;
980 case SSI_ELIF: {
981 const char *expr = NULL;
982 for (i = 2; i < n; i += 2) {
983 if (0 == strcmp(l[i], "expr")) {
984 expr = l[i+1];
985 } else {
986 log_error_write(srv, __FILE__, __LINE__, "sss",
987 "ssi: unknown attribute for ",
988 l[1], l[i]);
992 if (!expr) {
993 log_error_write(srv, __FILE__, __LINE__, "sss",
994 "ssi: ",
995 l[1], "expr missing");
996 break;
999 p->if_level--;
1001 if (p->if_level == p->if_is_false_level) {
1002 if ((p->if_is_false) &&
1003 (p->if_is_false_endif == 0)) {
1004 switch (ssi_eval_expr(srv, con, p, expr)) {
1005 case -1:
1006 case 0:
1007 p->if_is_false = 1;
1008 p->if_is_false_level = p->if_level;
1009 break;
1010 case 1:
1011 p->if_is_false = 0;
1012 break;
1014 } else {
1015 p->if_is_false = 1;
1016 p->if_is_false_level = p->if_level;
1017 p->if_is_false_endif = 1;
1021 p->if_level++;
1023 break;
1025 case SSI_ENDIF:
1026 p->if_level--;
1028 if (p->if_level == p->if_is_false_level) {
1029 p->if_is_false = 0;
1030 p->if_is_false_endif = 0;
1033 break;
1034 default:
1035 log_error_write(srv, __FILE__, __LINE__, "ss",
1036 "ssi: unknown ssi-command:",
1037 l[1]);
1038 break;
1041 return 0;
1045 static int mod_ssi_parse_ssi_stmt_value(const char * const s, const int len) {
1046 int n;
1047 const int c = (s[0] == '"' ? '"' : s[0] == '\'' ? '\'' : 0);
1048 if (0 != c) {
1049 for (n = 1; n < len; ++n) {
1050 if (s[n] == c) return n+1;
1051 if (s[n] == '\\') {
1052 if (n+1 == len) return 0; /* invalid */
1053 ++n;
1056 return 0; /* invalid */
1057 } else {
1058 for (n = 0; n < len; ++n) {
1059 if (isspace(s[n])) return n;
1060 if (s[n] == '\\') {
1061 if (n+1 == len) return 0; /* invalid */
1062 ++n;
1065 return n;
1069 static int mod_ssi_parse_ssi_stmt_offlen(int o[10], const char * const s, const int len) {
1072 * <!--#element attribute=value attribute=value ... -->
1075 /* s must begin "<!--#" and must end with "-->" */
1076 int n = 5;
1077 o[0] = n;
1078 for (; light_isalpha(s[n]); ++n) ; /*(n = 5 to begin after "<!--#")*/
1079 o[1] = n - o[0];
1080 if (0 == o[1]) return -1; /* empty token */
1082 if (n+3 == len) return 2; /* token only; no params */
1083 if (!isspace(s[n])) return -1;
1084 do { ++n; } while (isspace(s[n])); /* string ends "-->", so n < len */
1085 if (n+3 == len) return 2; /* token only; no params */
1087 o[2] = n;
1088 for (; light_isalpha(s[n]); ++n) ;
1089 o[3] = n - o[2];
1090 if (0 == o[3] || s[n++] != '=') return -1;
1092 o[4] = n;
1093 o[5] = mod_ssi_parse_ssi_stmt_value(s+n, len-n-3);
1094 if (0 == o[5]) return -1; /* empty or invalid token */
1095 n += o[5];
1097 if (n+3 == len) return 6; /* token and one param */
1098 if (!isspace(s[n])) return -1;
1099 do { ++n; } while (isspace(s[n])); /* string ends "-->", so n < len */
1100 if (n+3 == len) return 6; /* token and one param */
1102 o[6] = n;
1103 for (; light_isalpha(s[n]); ++n) ;
1104 o[7] = n - o[6];
1105 if (0 == o[7] || s[n++] != '=') return -1;
1107 o[8] = n;
1108 o[9] = mod_ssi_parse_ssi_stmt_value(s+n, len-n-3);
1109 if (0 == o[9]) return -1; /* empty or invalid token */
1110 n += o[9];
1112 if (n+3 == len) return 10; /* token and two params */
1113 if (!isspace(s[n])) return -1;
1114 do { ++n; } while (isspace(s[n])); /* string ends "-->", so n < len */
1115 if (n+3 == len) return 10; /* token and two params */
1116 return -1;
1119 static void mod_ssi_parse_ssi_stmt(server *srv, connection *con, plugin_data *p, char *s, int len, struct stat *st) {
1122 * <!--#element attribute=value attribute=value ... -->
1125 int o[10];
1126 int m;
1127 const int n = mod_ssi_parse_ssi_stmt_offlen(o, s, len);
1128 char *l[6] = { s, NULL, NULL, NULL, NULL, NULL };
1129 if (-1 == n) {
1130 /* XXX: perhaps emit error comment instead of invalid <!--#...--> code to client */
1131 chunkqueue_append_mem(con->write_queue, s, len); /* append stmt as-is */
1132 return;
1135 #if 0
1136 /* dup s and then modify s */
1137 /*(l[0] is no longer used; was previously used in only one place for error reporting)*/
1138 l[0] = malloc((size_t)(len+1));
1139 memcpy(l[0], s, (size_t)len);
1140 (l[0])[len] = '\0';
1141 #endif
1143 /* modify s in-place to split string into arg tokens */
1144 for (m = 0; m < n; m += 2) {
1145 char *ptr = s+o[m];
1146 switch (*ptr) {
1147 case '"':
1148 case '\'': (++ptr)[o[m+1]-2] = '\0'; break;
1149 default: ptr[o[m+1]] = '\0'; break;
1151 l[1+(m>>1)] = ptr;
1152 if (m == 4 || m == 8) {
1153 /* XXX: removing '\\' escapes from param value would be
1154 * the right thing to do, but would potentially change
1155 * current behavior, e.g. <!--#exec cmd=... --> */
1159 process_ssi_stmt(srv, con, p, (const char **)l, 1+(n>>1), st);
1161 #if 0
1162 free(l[0]);
1163 #endif
1166 static int mod_ssi_stmt_len(const char *s, const int len) {
1167 /* s must begin "<!--#" */
1168 int n, sq = 0, dq = 0, bs = 0;
1169 for (n = 5; n < len; ++n) { /*(n = 5 to begin after "<!--#")*/
1170 switch (s[n]) {
1171 default:
1172 break;
1173 case '-':
1174 if (!sq && !dq && n+2 < len && s[n+1] == '-' && s[n+2] == '>') return n+3; /* found end of stmt */
1175 break;
1176 case '"':
1177 if (!sq && (!dq || !bs)) dq = !dq; break;
1178 case '\'':
1179 if (!dq && (!sq || !bs)) sq = !sq; break;
1180 case '\\':
1181 if (sq || dq) bs = !bs; break;
1184 return 0; /* incomplete directive "<!--#...-->" */
1187 static void mod_ssi_read_fd(server *srv, connection *con, plugin_data *p, int fd, struct stat *st) {
1188 ssize_t rd;
1189 size_t offset, pretag;
1190 char buf[8192];
1192 offset = 0;
1193 pretag = 0;
1194 while (0 < (rd = read(fd, buf+offset, sizeof(buf)-offset))) {
1195 char *s;
1196 size_t prelen = 0, len;
1197 offset += (size_t)rd;
1198 for (; (s = memchr(buf+prelen, '<', offset-prelen)); ++prelen) {
1199 prelen = s - buf;
1200 if (prelen + 5 <= offset) { /*("<!--#" is 5 chars)*/
1201 if (0 != memcmp(s+1, CONST_STR_LEN("!--#"))) continue; /* loop to loop for next '<' */
1203 if (prelen - pretag && !p->if_is_false) {
1204 chunkqueue_append_mem(con->write_queue, buf+pretag, prelen-pretag);
1207 len = mod_ssi_stmt_len(buf+prelen, offset-prelen);
1208 if (len) { /* num of chars to be consumed */
1209 mod_ssi_parse_ssi_stmt(srv, con, p, buf+prelen, len, st);
1210 prelen += (len - 1); /* offset to '>' at end of SSI directive; incremented at top of loop */
1211 pretag = prelen + 1;
1212 if (pretag == offset) {
1213 offset = pretag = 0;
1214 break;
1216 } else if (0 == prelen && offset == sizeof(buf)) { /*(full buf)*/
1217 /* SSI statement is way too long
1218 * NOTE: skipping this buf will expose *the rest* of this SSI statement */
1219 chunkqueue_append_mem(con->write_queue, CONST_STR_LEN("<!-- [an error occurred: directive too long] "));
1220 /* check if buf ends with "-" or "--" which might be part of "-->"
1221 * (buf contains at least 5 chars for "<!--#") */
1222 if (buf[offset-2] == '-' && buf[offset-1] == '-') {
1223 chunkqueue_append_mem(con->write_queue, CONST_STR_LEN("--"));
1224 } else if (buf[offset-1] == '-') {
1225 chunkqueue_append_mem(con->write_queue, CONST_STR_LEN("-"));
1227 offset = pretag = 0;
1228 break;
1229 } else { /* incomplete directive "<!--#...-->" */
1230 memmove(buf, buf+prelen, (offset -= prelen));
1231 pretag = 0;
1232 break;
1234 } else if (prelen + 1 == offset || 0 == memcmp(s+1, "!--", offset - prelen - 1)) {
1235 if (prelen - pretag && !p->if_is_false) {
1236 chunkqueue_append_mem(con->write_queue, buf+pretag, prelen-pretag);
1238 memcpy(buf, buf+prelen, (offset -= prelen));
1239 pretag = 0;
1240 break;
1242 /* loop to look for next '<' */
1244 if (offset == sizeof(buf)) {
1245 if (!p->if_is_false) {
1246 chunkqueue_append_mem(con->write_queue, buf+pretag, offset-pretag);
1248 offset = pretag = 0;
1252 if (0 != rd) {
1253 log_error_write(srv, __FILE__, __LINE__, "SsB", "read(): ", strerror(errno), con->physical.path);
1256 if (offset - pretag) {
1257 /* copy remaining data in buf */
1258 if (!p->if_is_false) {
1259 chunkqueue_append_mem(con->write_queue, buf+pretag, offset-pretag);
1265 /* don't want to block when open()ing a fifo */
1266 #if defined(O_NONBLOCK)
1267 # define FIFO_NONBLOCK O_NONBLOCK
1268 #else
1269 # define FIFO_NONBLOCK 0
1270 #endif
1272 static int mod_ssi_handle_request(server *srv, connection *con, plugin_data *p) {
1273 int fd;
1274 struct stat st;
1276 /* get a stream to the file */
1278 array_reset(p->ssi_vars);
1279 array_reset(p->ssi_cgi_env);
1280 buffer_copy_string_len(p->timefmt, CONST_STR_LEN("%a, %d %b %Y %H:%M:%S %Z"));
1281 p->sizefmt = 0;
1282 build_ssi_cgi_vars(srv, con, p);
1283 p->if_is_false = 0;
1285 /* Reset the modified time of included files */
1286 include_file_last_mtime = 0;
1288 if (-1 == (fd = open(con->physical.path->ptr, O_RDONLY | FIFO_NONBLOCK))) {
1289 log_error_write(srv, __FILE__, __LINE__, "sb",
1290 "open: ", con->physical.path);
1291 return -1;
1294 if (0 != fstat(fd, &st)) {
1295 log_error_write(srv, __FILE__, __LINE__, "SB", "fstat failed: ", con->physical.path);
1296 close(fd);
1297 return -1;
1300 mod_ssi_read_fd(srv, con, p, fd, &st);
1302 close(fd);
1303 con->file_started = 1;
1304 con->file_finished = 1;
1305 con->mode = p->id;
1307 if (buffer_string_is_empty(p->conf.content_type)) {
1308 response_header_overwrite(srv, con, CONST_STR_LEN("Content-Type"), CONST_STR_LEN("text/html"));
1309 } else {
1310 response_header_overwrite(srv, con, CONST_STR_LEN("Content-Type"), CONST_BUF_LEN(p->conf.content_type));
1313 if (p->conf.conditional_requests) {
1314 /* Generate "ETag" & "Last-Modified" headers */
1315 buffer *mtime = NULL;
1317 /* use most recently modified include file for ETag and Last-Modified */
1318 if (st.st_mtime < include_file_last_mtime)
1319 st.st_mtime = include_file_last_mtime;
1321 etag_create(con->physical.etag, &st, con->etag_flags);
1322 response_header_overwrite(srv, con, CONST_STR_LEN("ETag"), CONST_BUF_LEN(con->physical.etag));
1324 mtime = strftime_cache_get(srv, st.st_mtime);
1325 response_header_overwrite(srv, con, CONST_STR_LEN("Last-Modified"), CONST_BUF_LEN(mtime));
1327 if (HANDLER_FINISHED == http_response_handle_cachable(srv, con, mtime)) {
1328 /* ok, the client already has our content,
1329 * no need to send it again */
1331 chunkqueue_reset(con->write_queue);
1335 /* Reset the modified time of included files */
1336 include_file_last_mtime = 0;
1338 /* reset physical.path */
1339 buffer_reset(con->physical.path);
1341 return 0;
1344 #define PATCH(x) \
1345 p->conf.x = s->x;
1346 static int mod_ssi_patch_connection(server *srv, connection *con, plugin_data *p) {
1347 size_t i, j;
1348 plugin_config *s = p->config_storage[0];
1350 PATCH(ssi_extension);
1351 PATCH(content_type);
1352 PATCH(conditional_requests);
1353 PATCH(ssi_exec);
1355 /* skip the first, the global context */
1356 for (i = 1; i < srv->config_context->used; i++) {
1357 data_config *dc = (data_config *)srv->config_context->data[i];
1358 s = p->config_storage[i];
1360 /* condition didn't match */
1361 if (!config_check_cond(srv, con, dc)) continue;
1363 /* merge config */
1364 for (j = 0; j < dc->value->used; j++) {
1365 data_unset *du = dc->value->data[j];
1367 if (buffer_is_equal_string(du->key, CONST_STR_LEN("ssi.extension"))) {
1368 PATCH(ssi_extension);
1369 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("ssi.content-type"))) {
1370 PATCH(content_type);
1371 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("ssi.conditional-requests"))) {
1372 PATCH(conditional_requests);
1373 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("ssi.exec"))) {
1374 PATCH(ssi_exec);
1379 return 0;
1381 #undef PATCH
1383 URIHANDLER_FUNC(mod_ssi_physical_path) {
1384 plugin_data *p = p_d;
1385 size_t k;
1387 if (con->mode != DIRECT) return HANDLER_GO_ON;
1389 if (buffer_is_empty(con->physical.path)) return HANDLER_GO_ON;
1391 mod_ssi_patch_connection(srv, con, p);
1393 for (k = 0; k < p->conf.ssi_extension->used; k++) {
1394 data_string *ds = (data_string *)p->conf.ssi_extension->data[k];
1396 if (buffer_is_empty(ds->value)) continue;
1398 if (buffer_is_equal_right_len(con->physical.path, ds->value, buffer_string_length(ds->value))) {
1399 /* handle ssi-request */
1401 if (mod_ssi_handle_request(srv, con, p)) {
1402 /* on error */
1403 con->http_status = 500;
1404 con->mode = DIRECT;
1407 return HANDLER_FINISHED;
1411 /* not found */
1412 return HANDLER_GO_ON;
1415 /* this function is called at dlopen() time and inits the callbacks */
1417 int mod_ssi_plugin_init(plugin *p);
1418 int mod_ssi_plugin_init(plugin *p) {
1419 p->version = LIGHTTPD_VERSION_ID;
1420 p->name = buffer_init_string("ssi");
1422 p->init = mod_ssi_init;
1423 p->handle_subrequest_start = mod_ssi_physical_path;
1424 p->set_defaults = mod_ssi_set_defaults;
1425 p->cleanup = mod_ssi_free;
1427 p->data = NULL;
1429 return 0;