thttpd/cgi-src/ssi.c

764 lines
18 KiB
C

/* ssi - server-side-includes CGI program
**
** Copyright © 1995 by Jef Poskanzer <jef@mail.acme.com>.
** All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
** 1. Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** 2. Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in the
** documentation and/or other materials provided with the distribution.
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
** ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
** OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
** HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
** SUCH DAMAGE.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include "config.h"
#include "match.h"
#define ST_GROUND 0
#define ST_LESSTHAN 1
#define ST_BANG 2
#define ST_MINUS1 3
#define ST_MINUS2 4
static void read_file( char* vfilename, char* filename, FILE* fp );
static char* argv0;
static char* url;
static char timefmt[100];
static int sizefmt;
#define SF_BYTES 0
#define SF_ABBREV 1
static struct stat sb;
static void
internal_error( char* reason )
{
char* title = "500 Internal Error";
(void) printf( "\
<HTML><HEAD><TITLE>%s</TITLE></HEAD>\n\
<BODY><H2>%s</H2>\n\
Something unusual went wrong during a server-side-includes request:\n\
<BLOCKQUOTE>\n\
%s\n\
</BLOCKQUOTE>\n\
</BODY></HTML>\n", title, title, reason );
}
static void
not_found( char* filename )
{
char* title = "404 Not Found";
(void) printf( "\
<HTML><HEAD><TITLE>%s</TITLE></HEAD>\n\
<BODY><H2>%s</H2>\n\
The requested server-side-includes filename, %s,\n\
does not seem to exist.\n\
</BODY></HTML>\n", title, title, filename );
}
static void
not_found2( char* directive, char* tag, char* filename2 )
{
char* title = "Not Found";
(void) printf( "\
<HR><H2>%s</H2>\n\
The filename requested in a %s %s directive, %s,\n\
does not seem to exist.\n\
<HR>\n", title, directive, tag, filename2 );
}
static void
not_permitted( char* directive, char* tag, char* val )
{
char* title = "Not Permitted";
(void) printf( "\
<HR><H2>%s</H2>\n\
The filename requested in the %s %s=%s directive\n\
may not be fetched.\n\
<HR>\n", title, directive, tag, val );
}
static void
unknown_directive( char* filename, char* directive )
{
char* title = "Unknown Directive";
(void) printf( "\
<HR><H2>%s</H2>\n\
The requested server-side-includes filename, %s,\n\
tried to use an unknown directive, %s.\n\
<HR>\n", title, filename, directive );
}
static void
unknown_tag( char* filename, char* directive, char* tag )
{
char* title = "Unknown Tag";
(void) printf( "\
<HR><H2>%s</H2>\n\
The requested server-side-includes filename, %s,\n\
tried to use the directive %s with an unknown tag, %s.\n\
<HR>\n", title, filename, directive, tag );
}
static void
unknown_value( char* filename, char* directive, char* tag, char* val )
{
char* title = "Unknown Value";
(void) printf( "\
<HR><H2>%s</H2>\n\
The requested server-side-includes filename, %s,\n\
tried to use the directive %s %s with an unknown value, %s.\n\
<HR>\n", title, filename, directive, tag, val );
}
static int
get_filename( char* vfilename, char* filename, char* directive, char* tag, char* val, char* fn, int fnsize )
{
int vl, fl;
char* cp;
/* Used for the various commands that accept a file name.
** These commands accept two tags:
** virtual
** Gives a virtual path to a document on the server.
** file
** Gives a pathname relative to the current directory. ../ cannot
** be used in this pathname, nor can absolute paths be used.
*/
vl = strlen( vfilename );
fl = strlen( filename );
if ( strcmp( tag, "virtual" ) == 0 )
{
if ( strstr( val, "../" ) != (char*) 0 )
{
not_permitted( directive, tag, val );
return -1;
}
/* Figure out root using difference between vfilename and filename. */
if ( vl > fl ||
strcmp( vfilename, &filename[fl - vl] ) != 0 )
return -1;
if ( fl - vl + strlen( val ) >= fnsize )
return -1;
(void) strncpy( fn, filename, fl - vl );
(void) strcpy( &fn[fl - vl], val );
}
else if ( strcmp( tag, "file" ) == 0 )
{
if ( val[0] == '/' || strstr( val, "../" ) != (char*) 0 )
{
not_permitted( directive, tag, val );
return -1;
}
if ( fl + 1 + strlen( val ) >= fnsize )
return -1;
(void) strcpy( fn, filename );
cp = strrchr( fn, '/' );
if ( cp == (char*) 0 )
{
cp = &fn[strlen( fn )];
*cp = '/';
}
(void) strcpy( ++cp, val );
}
else
{
unknown_tag( filename, directive, tag );
return -1;
}
return 0;
}
static int
check_filename( char* filename )
{
static int inited = 0;
static char* cgi_pattern;
int fnl;
char* cp;
char* dirname;
char* authname;
struct stat sb2;
int r;
if ( ! inited )
{
/* Get the cgi pattern. */
cgi_pattern = getenv( "CGI_PATTERN" );
#ifdef CGI_PATTERN
if ( cgi_pattern == (char*) 0 )
cgi_pattern = CGI_PATTERN;
#endif /* CGI_PATTERN */
inited = 1;
}
/* ../ is not permitted. */
if ( strstr( filename, "../" ) != (char*) 0 )
return 0;
#ifdef AUTH_FILE
/* Ensure that we are not reading a basic auth password file. */
fnl = strlen(filename);
if ( strcmp( filename, AUTH_FILE ) == 0 ||
( fnl >= sizeof(AUTH_FILE) &&
strcmp( &filename[fnl - sizeof(AUTH_FILE) + 1], AUTH_FILE ) == 0 &&
filename[fnl - sizeof(AUTH_FILE)] == '/' ) )
return 0;
/* Check for an auth file in the same directory. We can't do an actual
** auth password check here because CGI programs are not given the
** authorization header, for security reasons. So instead we just
** prohibit access to all auth-protected files.
*/
dirname = strdup( filename );
if ( dirname == (char*) 0 )
return 0; /* out of memory */
cp = strrchr( dirname, '/' );
if ( cp == (char*) 0 )
(void) strcpy( dirname, "." );
else
*cp = '\0';
authname = malloc( strlen( dirname ) + 1 + sizeof(AUTH_FILE) );
if ( authname == (char*) 0 )
return 0; /* out of memory */
(void) sprintf( authname, "%s/%s", dirname, AUTH_FILE );
r = stat( authname, &sb2 );
free( dirname );
free( authname );
if ( r == 0 )
return 0;
#endif /* AUTH_FILE */
/* Ensure that we are not reading a CGI file. */
if ( cgi_pattern != (char*) 0 && match( cgi_pattern, filename ) )
return 0;
return 1;
}
static void
show_time( time_t t, int gmt )
{
struct tm* tmP;
char tbuf[500];
if ( gmt )
tmP = gmtime( &t );
else
tmP = localtime( &t );
if ( strftime( tbuf, sizeof(tbuf), timefmt, tmP ) > 0 )
(void) fputs( tbuf, stdout );
}
static void
show_size( off_t size )
{
switch ( sizefmt )
{
case SF_BYTES:
(void) printf( "%ld", (long) size ); /* spec says should have commas */
break;
case SF_ABBREV:
if ( size < 1024 )
(void) printf( "%ld", (long) size );
else if ( size < 1024 )
(void) printf( "%ldK", (long) size / 1024L );
else if ( size < 1024*1024 )
(void) printf( "%ldM", (long) size / (1024L*1024L) );
else
(void) printf( "%ldG", (long) size / (1024L*1024L*1024L) );
break;
}
}
static void
do_config( char* vfilename, char* filename, FILE* fp, char* directive, char* tag, char* val )
{
/* The config directive controls various aspects of the file parsing.
** There are two valid tags:
** timefmt
** Gives the server a new format to use when providing dates. This
** is a string compatible with the strftime library call.
** sizefmt
** Determines the formatting to be used when displaying the size of
** a file. Valid choices are bytes, for a formatted byte count
** (formatted as 1,234,567), or abbrev for an abbreviated version
** displaying the number of kilobytes or megabytes the file occupies.
*/
if ( strcmp( tag, "timefmt" ) == 0 )
{
(void) strncpy( timefmt, val, sizeof(timefmt) - 1 );
timefmt[sizeof(timefmt) - 1] = '\0';
}
else if ( strcmp( tag, "sizefmt" ) == 0 )
{
if ( strcmp( val, "bytes" ) == 0 )
sizefmt = SF_BYTES;
else if ( strcmp( val, "abbrev" ) == 0 )
sizefmt = SF_ABBREV;
else
unknown_value( filename, directive, tag, val );
}
else
unknown_tag( filename, directive, tag );
}
static void
do_include( char* vfilename, char* filename, FILE* fp, char* directive, char* tag, char* val )
{
char vfilename2[1000];
char filename2[1000];
FILE* fp2;
/* Inserts the text of another document into the parsed document. */
if ( get_filename(
vfilename, filename, directive, tag, val, filename2,
sizeof(filename2) ) < 0 )
return;
if ( ! check_filename( filename2 ) )
{
not_permitted( directive, tag, filename2 );
return;
}
fp2 = fopen( filename2, "r" );
if ( fp2 == (FILE*) 0 )
{
not_found2( directive, tag, filename2 );
return;
}
if ( strcmp( tag, "virtual" ) == 0 )
{
if ( strlen( val ) < sizeof( vfilename2 ) )
(void) strcpy( vfilename2, val );
else
(void) strcpy( vfilename2, filename2 ); /* same size, has to fit */
}
else
{
if ( strlen( vfilename ) + 1 + strlen( val ) < sizeof(vfilename2) )
{
char* cp;
(void) strcpy( vfilename2, vfilename );
cp = strrchr( vfilename2, '/' );
if ( cp == (char*) 0 )
{
cp = &vfilename2[strlen( vfilename2 )];
*cp = '/';
}
(void) strcpy( ++cp, val );
}
else
(void) strcpy( vfilename2, filename2 ); /* same size, has to fit */
}
read_file( vfilename2, filename2, fp2 );
(void) fclose( fp2 );
}
static void
do_echo( char* vfilename, char* filename, FILE* fp, char* directive, char* tag, char* val )
{
char* cp;
time_t t;
/* Prints the value of one of the include variables. Any dates are
** printed subject to the currently configured timefmt. The only valid
** tag is var, whose value is the name of the variable you wish to echo.
*/
if ( strcmp( tag, "var" ) != 0 )
unknown_tag( filename, directive, tag );
else
{
if ( strcmp( val, "DOCUMENT_NAME" ) == 0 )
{
/* The current filename. */
(void) fputs( filename, stdout );
}
else if ( strcmp( val, "DOCUMENT_URI" ) == 0 )
{
/* The virtual path to this file (such as /~robm/foo.shtml). */
(void) fputs( vfilename, stdout );
}
else if ( strcmp( val, "QUERY_STRING_UNESCAPED" ) == 0 )
{
/* The unescaped version of any search query the client sent. */
cp = getenv( "QUERY_STRING" );
if ( cp != (char*) 0 )
(void) fputs( cp, stdout );
}
else if ( strcmp( val, "DATE_LOCAL" ) == 0 )
{
/* The current date, local time zone. */
t = time( (time_t*) 0 );
show_time( t, 0 );
}
else if ( strcmp( val, "DATE_GMT" ) == 0 )
{
/* Same as DATE_LOCAL but in Greenwich mean time. */
t = time( (time_t*) 0 );
show_time( t, 1 );
}
else if ( strcmp( val, "LAST_MODIFIED" ) == 0 )
{
/* The last modification date of the current document. */
if ( fstat( fileno( fp ), &sb ) >= 0 )
show_time( sb.st_mtime, 0 );
}
else
{
/* Try an environment variable. */
cp = getenv( val );
if ( cp == (char*) 0 )
unknown_value( filename, directive, tag, val );
else
(void) fputs( cp, stdout );
}
}
}
static void
do_fsize( char* vfilename, char* filename, FILE* fp, char* directive, char* tag, char* val )
{
char filename2[1000];
/* Prints the size of the specified file. */
if ( get_filename(
vfilename, filename, directive, tag, val, filename2,
sizeof(filename2) ) < 0 )
return;
if ( stat( filename2, &sb ) < 0 )
{
not_found2( directive, tag, filename2 );
return;
}
show_size( sb.st_size );
}
static void
do_flastmod( char* vfilename, char* filename, FILE* fp, char* directive, char* tag, char* val )
{
char filename2[1000];
/* Prints the last modification date of the specified file. */
if ( get_filename(
vfilename, filename, directive, tag, val, filename2,
sizeof(filename2) ) < 0 )
return;
if ( stat( filename2, &sb ) < 0 )
{
not_found2( directive, tag, filename2 );
return;
}
show_time( sb.st_mtime, 0 );
}
static void
parse( char* vfilename, char* filename, FILE* fp, char* str )
{
char* directive;
char* cp;
int ntags;
char* tags[200];
int dirn;
#define DI_CONFIG 0
#define DI_INCLUDE 1
#define DI_ECHO 2
#define DI_FSIZE 3
#define DI_FLASTMOD 4
int i;
char* val;
directive = str;
directive += strspn( directive, " \t\n\r" );
ntags = 0;
cp = directive;
for (;;)
{
cp = strpbrk( cp, " \t\n\r\"" );
if ( cp == (char*) 0 )
break;
if ( *cp == '"' )
{
cp = strpbrk( cp + 1, "\"" );
++cp;
if ( *cp == '\0' )
break;
}
*cp++ = '\0';
cp += strspn( cp, " \t\n\r" );
if ( *cp == '\0' )
break;
if ( ntags < sizeof(tags)/sizeof(*tags) )
tags[ntags++] = cp;
}
if ( strcmp( directive, "config" ) == 0 )
dirn = DI_CONFIG;
else if ( strcmp( directive, "include" ) == 0 )
dirn = DI_INCLUDE;
else if ( strcmp( directive, "echo" ) == 0 )
dirn = DI_ECHO;
else if ( strcmp( directive, "fsize" ) == 0 )
dirn = DI_FSIZE;
else if ( strcmp( directive, "flastmod" ) == 0 )
dirn = DI_FLASTMOD;
else
{
unknown_directive( filename, directive );
return;
}
for ( i = 0; i < ntags; ++i )
{
if ( i > 0 )
putchar( ' ' );
val = strchr( tags[i], '=' );
if ( val == (char*) 0 )
val = "";
else
*val++ = '\0';
if ( *val == '"' && val[strlen( val ) - 1] == '"' )
{
val[strlen( val ) - 1] = '\0';
++val;
}
switch( dirn )
{
case DI_CONFIG:
do_config( vfilename, filename, fp, directive, tags[i], val );
break;
case DI_INCLUDE:
do_include( vfilename, filename, fp, directive, tags[i], val );
break;
case DI_ECHO:
do_echo( vfilename, filename, fp, directive, tags[i], val );
break;
case DI_FSIZE:
do_fsize( vfilename, filename, fp, directive, tags[i], val );
break;
case DI_FLASTMOD:
do_flastmod( vfilename, filename, fp, directive, tags[i], val );
break;
}
}
}
static void
slurp( char* vfilename, char* filename, FILE* fp )
{
char buf[1000];
int i;
int state;
int ich;
/* Now slurp in the rest of the comment from the input file. */
i = 0;
state = ST_GROUND;
while ( ( ich = getc( fp ) ) != EOF )
{
switch ( state )
{
case ST_GROUND:
if ( ich == '-' )
state = ST_MINUS1;
break;
case ST_MINUS1:
if ( ich == '-' )
state = ST_MINUS2;
else
state = ST_GROUND;
break;
case ST_MINUS2:
if ( ich == '>' )
{
buf[i - 2] = '\0';
parse( vfilename, filename, fp, buf );
return;
}
else if ( ich != '-' )
state = ST_GROUND;
break;
}
if ( i < sizeof(buf) - 1 )
buf[i++] = (char) ich;
}
}
static void
read_file( char* vfilename, char* filename, FILE* fp )
{
int ich;
int state;
/* Copy it to output, while running a state-machine to look for
** SSI directives.
*/
state = ST_GROUND;
while ( ( ich = getc( fp ) ) != EOF )
{
switch ( state )
{
case ST_GROUND:
if ( ich == '<' )
{ state = ST_LESSTHAN; continue; }
break;
case ST_LESSTHAN:
if ( ich == '!' )
{ state = ST_BANG; continue; }
else
{ state = ST_GROUND; putchar( '<' ); }
break;
case ST_BANG:
if ( ich == '-' )
{ state = ST_MINUS1; continue; }
else
{ state = ST_GROUND; (void) fputs ( "<!", stdout ); }
break;
case ST_MINUS1:
if ( ich == '-' )
{ state = ST_MINUS2; continue; }
else
{ state = ST_GROUND; (void) fputs ( "<!-", stdout ); }
break;
case ST_MINUS2:
if ( ich == '#' )
{
slurp( vfilename, filename, fp );
state = ST_GROUND;
continue;
}
else
{ state = ST_GROUND; (void) fputs ( "<!--", stdout ); }
break;
}
putchar( (char) ich );
}
}
int
main( int argc, char** argv )
{
char* script_name;
char* path_info;
char* path_translated;
FILE* fp;
argv0 = argv[0];
/* Default formats. */
(void) strcpy( timefmt, "%a %b %e %T %Z %Y" );
sizefmt = SF_BYTES;
/* The MIME type has to be text/html. */
(void) fputs( "Content-type: text/html\n\n", stdout );
/* Get the name that we were run as. */
script_name = getenv( "SCRIPT_NAME" );
if ( script_name == (char*) 0 )
{
internal_error( "Couldn't get SCRIPT_NAME environment variable." );
exit( 1 );
}
/* Append the PATH_INFO, if any, to get the full URL. */
path_info = getenv( "PATH_INFO" );
if ( path_info == (char*) 0 )
path_info = "";
url = (char*) malloc( strlen( script_name ) + strlen( path_info ) + 1 );
if ( url == (char*) 0 )
{
internal_error( "Out of memory." );
exit( 1 );
}
(void) sprintf( url, "%s%s", script_name, path_info );
/* Get the name of the file to parse. */
path_translated = getenv( "PATH_TRANSLATED" );
if ( path_translated == (char*) 0 )
{
internal_error( "Couldn't get PATH_TRANSLATED environment variable." );
exit( 1 );
}
if ( ! check_filename( path_translated ) )
{
not_permitted( "initial", "PATH_TRANSLATED", path_translated );
exit( 1 );
}
/* Open it. */
fp = fopen( path_translated, "r" );
if ( fp == (FILE*) 0 )
{
not_found( path_translated );
exit( 1 );
}
/* Read and handle the file. */
read_file( path_info, path_translated, fp );
(void) fclose( fp );
exit( 0 );
}