initial capture of my stuff
[rrq/thttpd.git] / cgi-src / ssi.c
1 /* ssi - server-side-includes CGI program
2 **
3 ** Copyright © 1995 by Jef Poskanzer <jef@mail.acme.com>.
4 ** All rights reserved.
5 **
6 ** Redistribution and use in source and binary forms, with or without
7 ** modification, are permitted provided that the following conditions
8 ** are met:
9 ** 1. Redistributions of source code must retain the above copyright
10 **    notice, this list of conditions and the following disclaimer.
11 ** 2. Redistributions in binary form must reproduce the above copyright
12 **    notice, this list of conditions and the following disclaimer in the
13 **    documentation and/or other materials provided with the distribution.
14 **
15 ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16 ** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 ** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18 ** ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19 ** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20 ** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21 ** OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22 ** HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23 ** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24 ** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25 ** SUCH DAMAGE.
26 */
27
28 #include <stdio.h>
29 #include <stdlib.h>
30 #include <string.h>
31 #include <time.h>
32 #include <sys/types.h>
33 #include <sys/stat.h>
34
35 #include "config.h"
36 #include "match.h"
37
38
39 #define ST_GROUND 0
40 #define ST_LESSTHAN 1
41 #define ST_BANG 2
42 #define ST_MINUS1 3
43 #define ST_MINUS2 4
44
45
46 static void read_file( char* vfilename, char* filename, FILE* fp );
47
48
49 static char* argv0;
50 static char* url;
51
52 static char timefmt[100];
53 static int sizefmt;
54 #define SF_BYTES 0
55 #define SF_ABBREV 1
56 static struct stat sb;
57
58
59 static void
60 internal_error( char* reason )
61     {
62     char* title = "500 Internal Error";
63
64     (void) printf( "\
65 <HTML><HEAD><TITLE>%s</TITLE></HEAD>\n\
66 <BODY><H2>%s</H2>\n\
67 Something unusual went wrong during a server-side-includes request:\n\
68 <BLOCKQUOTE>\n\
69 %s\n\
70 </BLOCKQUOTE>\n\
71 </BODY></HTML>\n", title, title, reason );
72     }
73
74
75 static void
76 not_found( char* filename )
77     {
78     char* title = "404 Not Found";
79
80     (void) printf( "\
81 <HTML><HEAD><TITLE>%s</TITLE></HEAD>\n\
82 <BODY><H2>%s</H2>\n\
83 The requested server-side-includes filename, %s,\n\
84 does not seem to exist.\n\
85 </BODY></HTML>\n", title, title, filename );
86     }
87
88
89 static void
90 not_found2( char* directive, char* tag, char* filename2 )
91     {
92     char* title = "Not Found";
93
94     (void) printf( "\
95 <HR><H2>%s</H2>\n\
96 The filename requested in a %s %s directive, %s,\n\
97 does not seem to exist.\n\
98 <HR>\n", title, directive, tag, filename2 );
99     }
100
101
102 static void
103 not_permitted( char* directive, char* tag, char* val )
104     {
105     char* title = "Not Permitted";
106
107     (void) printf( "\
108 <HR><H2>%s</H2>\n\
109 The filename requested in the %s %s=%s directive\n\
110 may not be fetched.\n\
111 <HR>\n", title, directive, tag, val );
112     }
113
114
115 static void
116 unknown_directive( char* filename, char* directive )
117     {
118     char* title = "Unknown Directive";
119
120     (void) printf( "\
121 <HR><H2>%s</H2>\n\
122 The requested server-side-includes filename, %s,\n\
123 tried to use an unknown directive, %s.\n\
124 <HR>\n", title, filename, directive );
125     }
126
127
128 static void
129 unknown_tag( char* filename, char* directive, char* tag )
130     {
131     char* title = "Unknown Tag";
132
133     (void) printf( "\
134 <HR><H2>%s</H2>\n\
135 The requested server-side-includes filename, %s,\n\
136 tried to use the directive %s with an unknown tag, %s.\n\
137 <HR>\n", title, filename, directive, tag );
138     }
139
140
141 static void
142 unknown_value( char* filename, char* directive, char* tag, char* val )
143     {
144     char* title = "Unknown Value";
145
146     (void) printf( "\
147 <HR><H2>%s</H2>\n\
148 The requested server-side-includes filename, %s,\n\
149 tried to use the directive %s %s with an unknown value, %s.\n\
150 <HR>\n", title, filename, directive, tag, val );
151     }
152
153
154 static int
155 get_filename( char* vfilename, char* filename, char* directive, char* tag, char* val, char* fn, int fnsize )
156     {
157     int vl, fl;
158     char* cp;
159
160     /* Used for the various commands that accept a file name.
161     ** These commands accept two tags:
162     **   virtual
163     **     Gives a virtual path to a document on the server.
164     **   file
165     **     Gives a pathname relative to the current directory. ../ cannot
166     **     be used in this pathname, nor can absolute paths be used.
167     */
168     vl = strlen( vfilename );
169     fl = strlen( filename );
170     if ( strcmp( tag, "virtual" ) == 0 )
171         {
172         if ( strstr( val, "../" ) != (char*) 0 )
173             {
174             not_permitted( directive, tag, val );
175             return -1;
176             }
177         /* Figure out root using difference between vfilename and filename. */
178         if ( vl > fl ||
179              strcmp( vfilename, &filename[fl - vl] ) != 0 )
180             return -1;
181         if ( fl - vl + strlen( val ) >= fnsize )
182             return -1;
183         (void) strncpy( fn, filename, fl - vl );
184         (void) strcpy( &fn[fl - vl], val );
185         }
186     else if ( strcmp( tag, "file" ) == 0 )
187         {
188         if ( val[0] == '/' || strstr( val, "../" ) != (char*) 0 )
189             {
190             not_permitted( directive, tag, val );
191             return -1;
192             }
193         if ( fl + 1 + strlen( val ) >= fnsize )
194             return -1;
195         (void) strcpy( fn, filename );
196         cp = strrchr( fn, '/' );
197         if ( cp == (char*) 0 )
198             {
199             cp = &fn[strlen( fn )];
200             *cp = '/';
201             }
202         (void) strcpy( ++cp, val );
203         }
204     else
205         {
206         unknown_tag( filename, directive, tag );
207         return -1;
208         }
209     return 0;
210     }
211
212
213 static int
214 check_filename( char* filename )
215     {
216     static int inited = 0;
217     static char* cgi_pattern;
218     int fnl;
219     char* cp;
220     char* dirname;
221     char* authname;
222     struct stat sb2;
223     int r;
224
225     if ( ! inited )
226         {
227         /* Get the cgi pattern. */
228         cgi_pattern = getenv( "CGI_PATTERN" );
229 #ifdef CGI_PATTERN
230         if ( cgi_pattern == (char*) 0 )
231             cgi_pattern = CGI_PATTERN;
232 #endif /* CGI_PATTERN */
233         inited = 1;
234         }
235
236     /* ../ is not permitted. */
237     if ( strstr( filename, "../" ) != (char*) 0 )
238         return 0;
239
240 #ifdef AUTH_FILE
241     /* Ensure that we are not reading a basic auth password file. */
242     fnl = strlen(filename);
243     if ( strcmp( filename, AUTH_FILE ) == 0 ||
244          ( fnl >= sizeof(AUTH_FILE) &&
245            strcmp( &filename[fnl - sizeof(AUTH_FILE) + 1], AUTH_FILE ) == 0 &&
246            filename[fnl - sizeof(AUTH_FILE)] == '/' ) )
247         return 0;
248
249     /* Check for an auth file in the same directory.  We can't do an actual
250     ** auth password check here because CGI programs are not given the
251     ** authorization header, for security reasons.  So instead we just
252     ** prohibit access to all auth-protected files.
253     */
254     dirname = strdup( filename );
255     if ( dirname == (char*) 0 )
256         return 0;       /* out of memory */
257     cp = strrchr( dirname, '/' );
258     if ( cp == (char*) 0 )
259         (void) strcpy( dirname, "." );
260     else
261         *cp = '\0';
262     authname = malloc( strlen( dirname ) + 1 + sizeof(AUTH_FILE) );
263     if ( authname == (char*) 0 )
264         return 0;       /* out of memory */
265     (void) sprintf( authname, "%s/%s", dirname, AUTH_FILE );
266     r = stat( authname, &sb2 );
267     free( dirname );
268     free( authname );
269     if ( r == 0 )
270         return 0;
271 #endif /* AUTH_FILE */
272
273     /* Ensure that we are not reading a CGI file. */
274     if ( cgi_pattern != (char*) 0 && match( cgi_pattern, filename ) )
275         return 0;
276
277     return 1;
278     }
279
280
281 static void
282 show_time( time_t t, int gmt )
283     {
284     struct tm* tmP;
285     char tbuf[500];
286
287     if ( gmt )
288         tmP = gmtime( &t );
289     else
290         tmP = localtime( &t );
291     if ( strftime( tbuf, sizeof(tbuf), timefmt, tmP ) > 0 )
292         (void) fputs( tbuf, stdout );
293     }
294
295
296 static void
297 show_size( off_t size )
298     {
299     switch ( sizefmt )
300         {
301         case SF_BYTES:
302         (void) printf( "%ld", (long) size );  /* spec says should have commas */
303         break;
304         case SF_ABBREV:
305         if ( size < 1024 )
306             (void) printf( "%ld", (long) size );
307         else if ( size < 1024 )
308             (void) printf( "%ldK", (long) size / 1024L );
309         else if ( size < 1024*1024 )
310             (void) printf( "%ldM", (long) size / (1024L*1024L) );
311         else
312             (void) printf( "%ldG", (long) size / (1024L*1024L*1024L) );
313         break;
314         }
315     }
316
317
318 static void
319 do_config( char* vfilename, char* filename, FILE* fp, char* directive, char* tag, char* val )
320     {
321     /* The config directive controls various aspects of the file parsing.
322     ** There are two valid tags:
323     **   timefmt
324     **     Gives the server a new format to use when providing dates.  This
325     **     is a string compatible with the strftime library call.
326     **   sizefmt
327     **     Determines the formatting to be used when displaying the size of
328     **     a file.  Valid choices are bytes, for a formatted byte count
329     **     (formatted as 1,234,567), or abbrev for an abbreviated version
330     **     displaying the number of kilobytes or megabytes the file occupies.
331     */
332
333     if ( strcmp( tag, "timefmt" ) == 0 )
334         {
335         (void) strncpy( timefmt, val, sizeof(timefmt) - 1 );
336         timefmt[sizeof(timefmt) - 1] = '\0';
337         }
338     else if ( strcmp( tag, "sizefmt" ) == 0 )
339         {
340         if ( strcmp( val, "bytes" ) == 0 )
341             sizefmt = SF_BYTES;
342         else if ( strcmp( val, "abbrev" ) == 0 )
343             sizefmt = SF_ABBREV;
344         else
345             unknown_value( filename, directive, tag, val );
346         }
347     else
348         unknown_tag( filename, directive, tag );
349     }
350
351
352 static void
353 do_include( char* vfilename, char* filename, FILE* fp, char* directive, char* tag, char* val )
354     {
355     char vfilename2[1000];
356     char filename2[1000];
357     FILE* fp2;
358
359     /* Inserts the text of another document into the parsed document. */
360
361     if ( get_filename(
362              vfilename, filename, directive, tag, val, filename2,
363              sizeof(filename2) ) < 0 )
364         return;
365
366     if ( ! check_filename( filename2 ) )
367         {
368         not_permitted( directive, tag, filename2 );
369         return;
370         }
371
372     fp2 = fopen( filename2, "r" );
373     if ( fp2 == (FILE*) 0 )
374         {
375         not_found2( directive, tag, filename2 );
376         return;
377         }
378
379     if ( strcmp( tag, "virtual" ) == 0 )
380         {
381         if ( strlen( val ) < sizeof( vfilename2 ) )
382             (void) strcpy( vfilename2, val );
383         else
384             (void) strcpy( vfilename2, filename2 );  /* same size, has to fit */
385         }
386     else
387         {
388         if ( strlen( vfilename ) + 1 + strlen( val ) < sizeof(vfilename2) )
389             {
390             char* cp;
391             (void) strcpy( vfilename2, vfilename );
392             cp = strrchr( vfilename2, '/' );
393             if ( cp == (char*) 0 )
394                 {
395                 cp = &vfilename2[strlen( vfilename2 )];
396                 *cp = '/';
397                 }
398             (void) strcpy( ++cp, val );
399             }
400         else
401             (void) strcpy( vfilename2, filename2 );  /* same size, has to fit */
402         }
403
404     read_file( vfilename2, filename2, fp2 );
405     (void) fclose( fp2 );
406     }
407
408
409 static void
410 do_echo( char* vfilename, char* filename, FILE* fp, char* directive, char* tag, char* val )
411     {
412     char* cp;
413     time_t t;
414
415     /* Prints the value of one of the include variables.  Any dates are
416     ** printed subject to the currently configured timefmt.  The only valid
417     ** tag is var, whose value is the name of the variable you wish to echo.
418     */
419
420     if ( strcmp( tag, "var" ) != 0 )
421         unknown_tag( filename, directive, tag );
422     else
423         {
424         if ( strcmp( val, "DOCUMENT_NAME" ) == 0 )
425             {
426             /* The current filename. */
427             (void) fputs( filename, stdout );
428             }
429         else if ( strcmp( val, "DOCUMENT_URI" ) == 0 )
430             {
431             /* The virtual path to this file (such as /~robm/foo.shtml). */
432             (void) fputs( vfilename, stdout );
433             }
434         else if ( strcmp( val, "QUERY_STRING_UNESCAPED" ) == 0 )
435             {
436             /* The unescaped version of any search query the client sent. */
437             cp = getenv( "QUERY_STRING" );
438             if ( cp != (char*) 0 )
439                 (void) fputs( cp, stdout );
440             }
441         else if ( strcmp( val, "DATE_LOCAL" ) == 0 )
442             {
443             /* The current date, local time zone. */
444             t = time( (time_t*) 0 );
445             show_time( t, 0 );
446             }
447         else if ( strcmp( val, "DATE_GMT" ) == 0 )
448             {
449             /* Same as DATE_LOCAL but in Greenwich mean time. */
450             t = time( (time_t*) 0 );
451             show_time( t, 1 );
452             }
453         else if ( strcmp( val, "LAST_MODIFIED" ) == 0 )
454             {
455             /* The last modification date of the current document. */
456             if ( fstat( fileno( fp ), &sb ) >= 0 )
457                 show_time( sb.st_mtime, 0 );
458             }
459         else
460             {
461             /* Try an environment variable. */
462             cp = getenv( val );
463             if ( cp == (char*) 0 )
464                 unknown_value( filename, directive, tag, val );
465             else
466                 (void) fputs( cp, stdout );
467             }
468         }
469     }
470
471
472 static void
473 do_fsize( char* vfilename, char* filename, FILE* fp, char* directive, char* tag, char* val )
474     {
475     char filename2[1000];
476
477     /* Prints the size of the specified file. */
478
479     if ( get_filename(
480              vfilename, filename, directive, tag, val, filename2,
481              sizeof(filename2) ) < 0 )
482         return;
483     if ( stat( filename2, &sb ) < 0 )
484         {
485         not_found2( directive, tag, filename2 );
486         return;
487         }
488     show_size( sb.st_size );
489     }
490
491
492 static void
493 do_flastmod( char* vfilename, char* filename, FILE* fp, char* directive, char* tag, char* val )
494     {
495     char filename2[1000];
496
497     /* Prints the last modification date of the specified file. */
498
499     if ( get_filename(
500              vfilename, filename, directive, tag, val, filename2,
501              sizeof(filename2) ) < 0 )
502         return;
503     if ( stat( filename2, &sb ) < 0 )
504         {
505         not_found2( directive, tag, filename2 );
506         return;
507         }
508     show_time( sb.st_mtime, 0 );
509     }
510
511
512 static void
513 parse( char* vfilename, char* filename, FILE* fp, char* str )
514     {
515     char* directive;
516     char* cp;
517     int ntags;
518     char* tags[200];
519     int dirn;
520 #define DI_CONFIG 0
521 #define DI_INCLUDE 1
522 #define DI_ECHO 2
523 #define DI_FSIZE 3
524 #define DI_FLASTMOD 4
525     int i;
526     char* val;
527
528     directive = str;
529     directive += strspn( directive, " \t\n\r" );
530
531     ntags = 0;
532     cp = directive;
533     for (;;)
534         {
535         cp = strpbrk( cp, " \t\n\r\"" );
536         if ( cp == (char*) 0 )
537             break;
538         if ( *cp == '"' )
539             {
540             cp = strpbrk( cp + 1, "\"" );
541             ++cp;
542             if ( *cp == '\0' )
543                 break;
544             }
545         *cp++ = '\0';
546         cp += strspn( cp, " \t\n\r" );
547         if ( *cp == '\0' )
548             break;
549         if ( ntags < sizeof(tags)/sizeof(*tags) )
550             tags[ntags++] = cp;
551         }
552     
553     if ( strcmp( directive, "config" ) == 0 )
554         dirn = DI_CONFIG;
555     else if ( strcmp( directive, "include" ) == 0 )
556         dirn = DI_INCLUDE;
557     else if ( strcmp( directive, "echo" ) == 0 )
558         dirn = DI_ECHO;
559     else if ( strcmp( directive, "fsize" ) == 0 )
560         dirn = DI_FSIZE;
561     else if ( strcmp( directive, "flastmod" ) == 0 )
562         dirn = DI_FLASTMOD;
563     else
564         {
565         unknown_directive( filename, directive );
566         return;
567         }
568
569     for ( i = 0; i < ntags; ++i )
570         {
571         if ( i > 0 )
572             putchar( ' ' );
573         val = strchr( tags[i], '=' );
574         if ( val == (char*) 0 )
575             val = "";
576         else
577             *val++ = '\0';
578         if ( *val == '"' && val[strlen( val ) - 1] == '"' )
579             {
580             val[strlen( val ) - 1] = '\0';
581             ++val;
582             }
583         switch( dirn )
584             {
585             case DI_CONFIG:
586             do_config( vfilename, filename, fp, directive, tags[i], val );
587             break;
588             case DI_INCLUDE:
589             do_include( vfilename, filename, fp, directive, tags[i], val );
590             break;
591             case DI_ECHO:
592             do_echo( vfilename, filename, fp, directive, tags[i], val );
593             break;
594             case DI_FSIZE:
595             do_fsize( vfilename, filename, fp, directive, tags[i], val );
596             break;
597             case DI_FLASTMOD:
598             do_flastmod( vfilename, filename, fp, directive, tags[i], val );
599             break;
600             }
601         }
602     }
603
604
605 static void
606 slurp( char* vfilename, char* filename, FILE* fp )
607     {
608     char buf[1000];
609     int i;
610     int state;
611     int ich;
612
613     /* Now slurp in the rest of the comment from the input file. */
614     i = 0;
615     state = ST_GROUND;
616     while ( ( ich = getc( fp ) ) != EOF )
617         {
618         switch ( state )
619             {
620             case ST_GROUND:
621             if ( ich == '-' )
622                 state = ST_MINUS1;
623             break;
624             case ST_MINUS1:
625             if ( ich == '-' )
626                 state = ST_MINUS2;
627             else
628                 state = ST_GROUND;
629             break;
630             case ST_MINUS2:
631             if ( ich == '>' )
632                 {
633                 buf[i - 2] = '\0';
634                 parse( vfilename, filename, fp, buf );
635                 return;
636                 }
637             else if ( ich != '-' )
638                 state = ST_GROUND;
639             break;
640             }
641         if ( i < sizeof(buf) - 1 )
642             buf[i++] = (char) ich;
643         }
644     }
645
646
647 static void
648 read_file( char* vfilename, char* filename, FILE* fp )
649     {
650     int ich;
651     int state;
652
653     /* Copy it to output, while running a state-machine to look for
654     ** SSI directives.
655     */
656     state = ST_GROUND;
657     while ( ( ich = getc( fp ) ) != EOF )
658         {
659         switch ( state )
660             {
661             case ST_GROUND:
662             if ( ich == '<' )
663                 { state = ST_LESSTHAN; continue; }
664             break;
665             case ST_LESSTHAN:
666             if ( ich == '!' )
667                 { state = ST_BANG; continue; }
668             else
669                 { state = ST_GROUND; putchar( '<' ); }
670             break;
671             case ST_BANG:
672             if ( ich == '-' )
673                 { state = ST_MINUS1; continue; }
674             else
675                 { state = ST_GROUND; (void) fputs ( "<!", stdout ); }
676             break;
677             case ST_MINUS1:
678             if ( ich == '-' )
679                 { state = ST_MINUS2; continue; }
680             else
681                 { state = ST_GROUND; (void) fputs ( "<!-", stdout ); }
682             break;
683             case ST_MINUS2:
684             if ( ich == '#' )
685                 {
686                 slurp( vfilename, filename, fp );
687                 state = ST_GROUND;
688                 continue;
689                 }
690             else
691                 { state = ST_GROUND; (void) fputs ( "<!--", stdout ); }
692             break;
693             }
694         putchar( (char) ich );
695         }
696     }
697
698
699 int
700 main( int argc, char** argv )
701     {
702     char* script_name;
703     char* path_info;
704     char* path_translated;
705     FILE* fp;
706
707     argv0 = argv[0];
708
709     /* Default formats. */
710     (void) strcpy( timefmt, "%a %b %e %T %Z %Y" );
711     sizefmt = SF_BYTES;
712
713     /* The MIME type has to be text/html. */
714     (void) fputs( "Content-type: text/html\n\n", stdout );
715
716     /* Get the name that we were run as. */
717     script_name = getenv( "SCRIPT_NAME" );
718     if ( script_name == (char*) 0 )
719         {
720         internal_error( "Couldn't get SCRIPT_NAME environment variable." );
721         exit( 1 );
722         }
723
724     /* Append the PATH_INFO, if any, to get the full URL. */
725     path_info = getenv( "PATH_INFO" );
726     if ( path_info == (char*) 0 )
727         path_info = "";
728     url = (char*) malloc( strlen( script_name ) + strlen( path_info ) + 1 );
729     if ( url == (char*) 0 )
730         {
731         internal_error( "Out of memory." );
732         exit( 1 );
733         }
734     (void) sprintf( url, "%s%s", script_name, path_info );
735
736     /* Get the name of the file to parse. */
737     path_translated = getenv( "PATH_TRANSLATED" );
738     if ( path_translated == (char*) 0 )
739         {
740         internal_error( "Couldn't get PATH_TRANSLATED environment variable." );
741         exit( 1 );
742         }
743
744     if ( ! check_filename( path_translated ) )
745         {
746         not_permitted( "initial", "PATH_TRANSLATED", path_translated );
747         exit( 1 );
748         }
749
750     /* Open it. */
751     fp = fopen( path_translated, "r" );
752     if ( fp == (FILE*) 0 )
753         {
754         not_found( path_translated );
755         exit( 1 );
756         }
757
758     /* Read and handle the file. */
759     read_file( path_info, path_translated, fp );
760
761     (void) fclose( fp );
762     exit( 0 );
763     }