#!/usr/bin/perl use strict; use warnings; =head1 NAME site-mason_handler.fcgi - run Mason on Dreamhost under FastCGI =head1 COPYRIGHT & LICENSE Copyright 2006-2010 Joe Pepersack (joe@pepersack.net) This program is free software; you may use it under the terms of the Creative Commons Attribution-Share Alike 3.0 United States License =head1 SYNOPSIS FastCGI handler script to use Mason on Dreamhost. It is assumed that this script will reside in $docroot/cgi-bin/ If you are running multiple mason sites on dreamhost, it may be useful to give each handler it's own name so that you can use ps or top to determine which process belongs to which site Some additional configuration is needed in .htaccess in order to use this. At a minimum you need something like: AddHandler fastcgi-script .fcgi Action html-mason /cgi-bin/site-masonhandler.fcgi SetHandler html-mason The following is highly recommended as well: Order allow,deny Deny from all Note while this is geared towards using FastCGI it will work just fine under regular CGI. Just change the AddHandler line in .htacces to: AddHandler cgi-script .fcgi This is highly useful when you are debugging, because under FastCGI, some errors will cause the browser to hang for 60 seconds until the session times out. Using CGI avoids this; revert to fastcgi when your bugs are squashed. Unfortunatly, dhandler processing is borked on dreamhost - requests for non-existant files get 404'ed before the mason handler ever fires. This means we can't simply use $mason->handle_cgi_object($cgi); To work around this, we can use mod_rewrite to send any request for anything that is not a file and not a directory to a dummy page. If you want dhandlers to work, the following is required in .htaccess: RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /dummy.html [L] Because this workaround breaks DirectoryIndex handling (which works correctly otherwise), the root dhandler should be something like: <%perl> my $page = $r->path_info; $page .= 'index.html' if $page =~ /\/$/; if ( $m->comp_exists($page) ) { $m->redirect($page); } else { $m->redirect('/error/404.html'); } This can be overridden in any subdirectory by giving it a 'real' dhandler You could use $m->comp instead of $m->redirect for index pages, but this breaks down if you have methods defined in your autohandler that you want to override in the target component. If you used $m->comp, the autohandler would inherit it's methods from the dhandler instead of the target component. As of mid-2008, Dreamhost has Mason pre-installed, at least on my host (Eastman), as well as many other CPAN modules. If you use any 'unsupported' cpan modules or a custom module (EG catalyst), you will have to add your module directory to @INC yourself. YMMV if you're on a different server. If you're on a private server, you should probably be using mod_perl instead of FastCGI anyway. =head1 BUGS For some unknown reason, setting decline_dirs = 0 blows up. Regardless, the necessity for the dhandler hack renders this a moot point. =head1 SEE ALSO The documentation for CGI::Fast and HTML::Mason =cut # --------------------------------------------------------------------------- # #### CHANGE THIS FOR YOUR SITE #### my $home = '/home/myname'; # ~ on your hosting account my $site = 'www.example.com'; # the name of your site # mason locations my $mason_root = "$home/$site/"; # where Mason components live (= docroot) my $mason_data = "$home/.masondata-$site/"; # where Mason data lives (not under docroot) my $logfile = "$home/mason-${site}.log"; # custom log (or set to undef to use CGI::Carp) # flags for optional features my $verbosity = 1; # enable custom logging level 0 - N my $usedb = 0; # create and use global db handle my $exit_requested = 0; # flag set by exit signal # module dependencies use CGI::Fast qw| :standard -compile :all -tabindex -utf8 |; use CGI::Carp; use HTML::Mason; use HTML::Mason::CGIHandler; use Time::HiRes qw| time gettimeofday |; use POSIX qw| strftime |; use DBI; # For Fastcgi, we need to use signal handling to catch PIPE, TERM, and USR1 # if we want to do an orderly shutdown. The END block only fires if we # are running under the regular cgi-handler. # See: http://www.fastcgi.com/docs/faq.html for more info $SIG{PIPE} = sub { logmsg("Caught SIGPIPE",0) }; # aborted connection $SIG{USR1} = \&fcgi_die; # graceful shutdown $SIG{TERM} = \&fcgi_die; # hard shutdown, also thrown after USR1 # turn logging on if so configured my $LOG; if ( $verbosity > 0 && defined($logfile) ) { open $LOG, '>>', $logfile; $LOG->autoflush(1); } logmsg( 'Starting FastCGI session', 0 ); # globals variables to share with mason # using $::VAR is safe under FastCGI; mod_perl, not so much $::DBH = init_db(); # persistent database handle $::STATS = {}; # record the time the session started and the process id push @{ $::STATS->{session} }, { init_time => now(), pid => $$ }; # variables defined under the mason_globals config option are global to Mason # but are not shared between Mason and the CGI wrapper # the values would likely be set in a <%once> section in a syshandler object. my (@quotes); $::MASON_GLOBALS = [ qw( @quotes %error %blog %gallery %tag %comment @scripts @styles %metadata %google @menu ) ]; my $mason = init_mason(); # Persistent HTML::Mason::CGIHandler object # the main loop - runs once per request. CGI::Fast->new blocks until a # request comes in. If Apache sees that all existing handlers are still # working when a new request comes in, it will spawn a new handler process. # Obviously, global variables are not shared between these separate processes. REQUEST: while ( my $cgi = CGI::Fast->new ) { $::START = time(); my $start = $::START; charset('utf-8'); # The dhandler workaround hack sets $ENV{PATH_INFO} to /dummy.html # if the user requests anything nonexistent, so we need to restore # the "real" value, which is in $ENV{REQUEST_URI} my $real_uri = $ENV{REQUEST_URI}; my $cgi_target = undef; # cgi-level redirect target, outside of mason # don't log keepalive requests. We can use a cron job which wget's # a keepalive url to help prevent the fcgi process from getting killed # This is a slightly naughty thing to do on a shared server if ( $real_uri !~ /^\/keepalive/ ) { ++$::STATS->{hits}; ++$::STATS->{views}{$real_uri}; logmsg( "Processing request $real_uri", 3 ); } if ( $real_uri eq '/dummy.html' ) { # if someone actually requested the dummy, give them an error $cgi_target = '/error/404.html'; # be sure this exists!!! } elsif ( $real_uri =~ /(handler|\.mas|\.dat|\.svn|\.htaccess|\.db)$/ ) { # forbid access to special files $cgi_target = '/error/403.html'; } else { # otherwise, revert to what was actually requested $ENV{PATH_INFO} = $real_uri; } # if the user tried to do something forbidden, do a cgi redirect # otherwise, let mason handle the request if ( defined($cgi_target) ) { print $cgi->redirect($cgi_target); } else { $mason->handle_request(); } # finally, spit out some benchmarking data; my $ms = elapsed_time($start); if ( $real_uri !~ /^\/keepalive/ ) { $::STATS->{time}{total} += $ms; $::STATS->{time}{$real_uri} += $ms; logmsg( sprintf( '%6.2f ms %s', $ms, $real_uri ), 1 ); } last REQUEST if $exit_requested; } logmsg( "Mason Handler exiting", 0 ); exit(0); # --------------------------------------------------------------------------- # sub fcgi_die { my $signal = shift; logmsg( "Shutting down FastCGI process via SIG$signal", 0 ); $::DBH->disconnect if $usedb; $exit_requested = 1; } # --------------------------------------------------------------------------- # sub init_mason { # initialize the mason object my $h; eval { logmsg( 'Initializing Mason CGIHandler', 2 ); $h = HTML::Mason::CGIHandler->new( comp_root => $mason_root, data_dir => $mason_data, allow_globals => $::MASON_GLOBALS, error_mode => "output", # For some reason, decline_dirs makes the cgi croak. Why? # decline_dirs => 0, default_escape_flags => "h" ); } or do { croak "Could not create mason cgihandler: $@"; }; return $h; } # --------------------------------------------------------------------------- # sub init_db { # create database handle return unless $usedb; # bail if db disabled logmsg( 'initializing db handle', 2 ); # read db config from file. This is a single line, pipe delimited # DatabaseName|HostName|UserName|Password open my $CRED, '<', "$home/.dbcredentials"; my $cred = <$CRED>; chomp($cred); my ( $db, $host, $username, $password ) = split( /\|/, $cred ); my $dsn = "DBI:mysql:database=$db;host=$host"; return ( DBI->connect_cached( $dsn, $username, $password ) ); close $CRED; } # --------------------------------------------------------------------------- # sub now { # get the system time and return it in ISO format (yyyy-mm-dd 23:59) return ( strftime( "%Y-%m-%d %T", localtime() ) ); } # --------------------------------------------------------------------------- # sub elapsed_time { # given a start time, return the elapsed time in milliseconds my $start = shift; return sprintf "%.3f", ( ( time() - $start ) * 1000 ); } # --------------------------------------------------------------------------- # sub logmsg { my ( $message, $level ) = @_; $level = 1 unless defined($level); # Write a message to the configured log if the message level is at the # current verbosity level or higher. # # This extra layer of logging may seem redundant, but many years spent in # systems operations have taught me the value of instrumenting your code # (particuarly when you get paged at 3am to resolve an issue) return if $level > $verbosity; # bail immediately if not verbose enough if ( defined $logfile ) { my $ts = now(); print $LOG "[$ts] $message\n"; } else { carp($message); } } # --------------------------------------------------------------------------- #