#!/usr/bin/perl # \file # cgissi.fcgi -- A FastCGI script to emulate Server-Side Includes # # At the time of writing, it supports #include, #fsize and #flastmod # use warnings; use strict; use Digest::MD5 qw(md5_base64); use File::Spec; use FCGI; use POSIX qw(strftime); # Content cache to avoid regenerating pages unless it's absolutely necessary # # This hash maps between the full local path to the URL data # and a hash containing cache data. For example: # # { "/foo/bar/baz" => # { last_modified => 0123456789, # data_digest => "base64(MD5($data))" # data_last_modified => 0123456789, # data => "generated page content", # dependencies => # { "/foo/bar/bat" => # { last_modified => 0123456789, # commands => # [ # { command => "include" | "fsize" | "flastmod", # start_offset = 123, # end_offset = 456 # }, # { command => "include" | "fsize" | "flastmod", # start_offset = 678, # end_offset = 901 # } # ] # } # } # } # } # my %cache; # Request object my $request = FCGI::Request(); # Loop until the server tells us to quit while ($request->Accept() >= 0) { # Get document root for this request my $docroot = $ENV{DOCUMENT_ROOT}; # Get path of item for this request # Yes, this is hacky. It appears that REDIRECT_PATH contains # the destination document resulting from content negotiation. # As this is an absolute path, we strip the document root from it. my ($path) = ($ENV{REDIRECT_PATH} =~ m/$docroot(.*)/); # Find item in cache my $cachedata = $cache{$docroot . $path}; # Determine if we need to (re)generate page validate_cache_entry($docroot, $path, $cachedata); # Refetch cache entry # (as it may have been created by validate_cache_entry) $cachedata = $cache{$docroot . $path}; # TODO It would be nice to send 304 responses where appropriate # and also to pay attention to conditional requests namely # If-(None-)Match and If-(Un)Modified-Since # Cache-related headers print "ETag: " . $$cachedata{data_digest} . "\r\n"; # Don't use %Z here, as perl's strftime appears to assume that the # time is always in the local timezone print "Last-Modified: " . strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime($$cachedata{data_last_modified})) . "\r\n"; # Send Content-Type header print "Content-Type: text/html; charset=ISO-8859-1\r\n"; print "\r\n"; # And the page data print $$cachedata{data}; } ################################################################################ # Run an include command # # \param filepath The full path of the file to include # \param data Data buffer to append file data to (reference) # sub do_include { my ($filepath, $data) = @_; if (-e $filepath) { # Unlike Zeus, we don't recursively apply SSI. open FILE, $filepath; { local $/ = undef; $$data .= ; } close FILE; } } # Run a fsize command # # \param filepath The full path of the file to measure the size of # \param data Data buffer to append size to (reference) # sub do_fsize { my ($filepath, $data) = @_; my @units = ( "B", "kB", "MB", "GB", "TB" ); # should be sufficient ;) if (-e $filepath) { my $size = -s $filepath; my $index = 0; while ($size > 1000) { $size /= 1024; $index++; } $$data .= sprintf("%.1f %s", $size, $units[$index]); } } # Run a flastmod command # # \param filepath The full path of the file to get the modification time of # \param data Data buffer to append modification time to (reference) # sub do_flastmod { my ($filepath, $data) = @_; if (-e $filepath) { my $mod_string = strftime("%d %b %Y %R", gmtime((stat($filepath))[9])); $$data .= $mod_string . " UTC"; } } # Process an include command # # \param command The command string to process # \param data Data buffer to append output to (reference) # \param docroot The document root # \param path Path of the current document, relative to ::docroot # \return The full path of the included file # sub process_include_command { my ($command, $data, $docroot, $path) = @_; my $filepath; if ($command =~ /file=/) { my ($relpath) = ($command =~ m/file="(.*)"/); my ($vol, $dirs, $file) = File::Spec->splitpath($docroot . $path); $filepath = File::Spec->rel2abs($relpath, File::Spec->catpath($vol, $dirs, "")); } elsif ($command =~ /virtual=/) { my ($vpath) = ($command =~ m/virtual="(.*)"/); if ($vpath =~ m#^/.*#) { $filepath = $docroot . $vpath; } else { my ($vol, $dirs, $file) = File::Spec->splitpath($docroot . $path); $filepath = File::Spec->rel2abs($vpath, File::Spec->catpath($vol, $dirs, "")); } } do_include($filepath, $data); return $filepath; } # Process an fsize command # # \param command The command string to process # \param data Data buffer to append output to (reference) # \param docroot The document root # \param path Path of the current document, relative to ::docroot # \return The full path of the included file # sub process_fsize_command { my ($command, $data, $docroot, $path) = @_; my $filepath; if ($command =~ /file=/) { my ($relpath) = ($command =~ m/file="(.*)"/); my ($vol, $dirs, $file) = File::Spec->splitpath($docroot . $path); $filepath = File::Spec->rel2abs($relpath, File::Spec->catpath($vol, $dirs, "")); } elsif ($command =~ /virtual=/) { my ($vpath) = ($command =~ m/virtual="(.*)"/); if ($vpath =~ m#^/.*#) { $filepath = $docroot . $vpath; } else { my ($vol, $dirs, $file) = File::Spec->splitpath($docroot . $path); $filepath = File::Spec->rel2abs($vpath, File::Spec->catpath($vol, $dirs, "")); } } do_fsize($filepath, $data); return $filepath; } # Process an flastmod command # # \param command The command string to process # \param data Data buffer to append output to (reference) # \param docroot The document root # \param path Path of the current document, relative to ::docroot # \return The full path of the included file # sub process_flastmod_command { my ($command, $data, $docroot, $path) = @_; my $filepath; if ($command =~ /file=/) { my ($relpath) = ($command =~ m/file="(.*)"/); my ($vol, $dirs, $file) = File::Spec->splitpath($docroot . $path); $filepath = File::Spec->rel2abs($relpath, File::Spec->catpath($vol, $dirs, "")); } elsif ($command =~ /virtual=/) { my ($vpath) = ($command =~ m/virtual="(.*)"/); if ($vpath =~ m#^/.*#) { $filepath = $docroot . $vpath; } else { my ($vol, $dirs, $file) = File::Spec->splitpath($docroot . $path); $filepath = File::Spec->rel2abs($vpath, File::Spec->catpath($vol, $dirs, "")); } } do_flastmod($filepath, $data); return $filepath; } # Complete parsing of and process a command string # # \param template Open file handle of template input # \param char Current input character # \param command The command string read so far # \param data Buffer to append data into (reference) # \param docroot The document root # \param path Path of the current document relative to ::docroot # \param start_offset Offset into template input of start of SSI command # \param cacheentry The cache entry for the current document # sub process_command { my ($template, $char, $command, $data, $docroot, $path, $start_offset, $cacheentry) = @_; my $keyword = lc($command); my $dash_count = 0; my $filepath; # See state machine documentation in generate_page_full for details of # how this function works. # Reject command if it's one we know nothing about if ($keyword ne "include" && $keyword ne "fsize" && $keyword ne "flastmod") { # ProcessCmd -> Initial transition occurs in generate_page_full $$data .= "