The Perl Advent Calendar needs more articles for 2022. Submit your idea today!
#!/usr/bin/perl
# This program is free software; you can redistribute it and/or modify it
# under the same terms as Perl itself.
#
#  (C) Paul Evans, 2010 -- leonerd@leonerd.org.uk

use strict;
use warnings;

use Getopt::Long;

use Net::FastCGI::IO qw( read_record );
use Net::FastCGI::Constant qw( :common :type :role );
use Net::FastCGI::Protocol qw(
   build_begin_request_body
   build_params
   parse_end_request_body
);

sub write_record
{
   Net::FastCGI::IO::write_record(@_) or
      die "Cannot write_record - $!";
}

my %env = (
   REQUEST_METHOD  => "GET",
   SCRIPT_NAME     => "",
   SERVER_NAME     => "server",
   SERVER_PORT     => 80,
   SERVER_PROTOCOL => "HTTP/1.1",
);

my $stdin_from;
my $filter_stdout;

sub usage
{
   print <<"EOF";
$0 [options] CONNECT URL

Runs the FastCGI found at CONNECT, as if it had received the URL

CONNECT may be any of

  exec:PATH             Execute as a child process with socket on STDIN
  unix:PATH             Find a UNIX socket on the given path
  tcp:HOST:PORT         Connect to the given port on the given host
  HOST:PORT              as above

options may be:

      --body            Print just the HTTP response body
      --no-body         Print just the HTTP response headers without the body
  -m, --method METHOD   Use the specified method (default "GET")
  -p, --post            Method is POST, pass STDIN
      --put             Method is PUT, pass STDIN
      --stdin PATH      Read STDIN from specified path, "-" means real script

EOF
}

GetOptions(
   'body' => sub {
      defined $filter_stdout and die "Cannot --no-body and --body\n";
      $filter_stdout = "body";
   },
   'no-body' => sub {
      defined $filter_stdout and die "Cannot --no-body and --body\n";
      $filter_stdout = "headers";
   },
   'm|method=s' => \$env{REQUEST_METHOD},
   'p|post' => sub {
      $env{REQUEST_METHOD} = "POST";
      $stdin_from = "-";
   },
   'put' => sub {
      $env{REQUEST_METHOD} = "PUT";
      $stdin_from = "-";
   },
   'stdin=s' => \$stdin_from,
   'help' => sub { usage; exit(0) },
) or exit(1);

my $connect = shift @ARGV or
   die "Require connection string\n";

my $url = shift @ARGV or
   die "Require a URL";

if( $url =~ s{^http(s?)://([^/:]+)(?::([^/]+))?}{} ) {
   $env{HTTPS} = "on" if $1;
   $env{SERVER_NAME} = $2;
   $env{SERVER_PORT} = $3 || ( $1 ? 443 : 80 );
}

$env{REQUEST_URI} = $url;

my ( $path, $query ) = $url =~ m/^(.*)(?:\?(.*))$/;

$env{PATH_INFO}    = $path;
$env{QUERY_STRING} = $query;

my $socket;

if( $connect =~ m/^unix:(.*)$/ ) {
   my $path = $1;

   require IO::Socket::UNIX;

   $socket = IO::Socket::UNIX->new(
      Peer => $path,
   ) or die "Cannot connect - $!\n";
}
elsif( $connect =~ m/^exec:(.*)$/ ) {
   my $script = $1;

   require IO::Socket::INET;

   my $listener = IO::Socket::INET->new(
      LocalHost => "localhost",
      Listen    => 1,
   ) or die "Cannot listen - $@";

   defined( my $kid = fork ) or die "Cannot fork - $!";
   END { defined $kid and kill TERM => $kid }

   if( $kid == 0 ) {
      close STDIN;
      open STDIN, "<&", $listener or die "Cannot dup $listener to STDIN - $!";

      close $listener;

      exec { $script } $script or die "Cannot exec $script - $!";
   }

   $socket = IO::Socket::INET->new(
      PeerHost => $listener->sockhost,
      PeerPort => $listener->sockport,
   ) or die "Cannot connect - $@";

   close $listener;
}
elsif( $connect =~ m/^(?:tcp:)?(.*):(.+?)$/ ) {
   my $host = $1 || "localhost";
   my $port = $2;

   my $class = eval { require IO::Socket::IP   and "IO::Socket::IP" } ||
               do   { require IO::Socket::INET and "IO::Socket::INET" };

   $socket = $class->new(
      PeerHost => $host,
      PeerPort => $port,
   ) or die "Cannot connect - $@\n";
}
else {
   die "Cannot recognise connection string '$connect'\n";
}

write_record( $socket, FCGI_BEGIN_REQUEST, 1,
   build_begin_request_body( FCGI_RESPONDER, 0 ) );

write_record( $socket, FCGI_PARAMS, 1,
   build_params( \%env ) );

write_record( $socket, FCGI_PARAMS, 1, "" );

if( defined $stdin_from ) {
   my $stdin;

   if( $stdin_from eq "-" ) {
      $stdin = \*STDIN;
   }
   else {
      open $stdin, "<", $stdin_from or die "Cannot open $stdin_from for input - $!";
   }

   while( read( $stdin, my $buffer, 8192 ) ) {
      write_record( $socket, FCGI_STDIN, 1, $buffer );
   }
}

write_record( $socket, FCGI_STDIN, 1, "" );

my $stdout = "";

while(1) {
   my ( $type, $id, $content ) = read_record( $socket )
      or $! and die "Cannot read_record - $!"
      or last;

   if( $type == FCGI_STDOUT ) {
      if( !defined $filter_stdout ) {
         print STDOUT $content;
      }
      elsif( $filter_stdout eq "headers" ) {
         my $oldlen = length $stdout;
         $stdout .= $content;
         if( $stdout =~ m/\r\n\r\n/ ) {
            # Print only the bit we haven't done yet
            print STDOUT substr( $stdout, $oldlen, $+[0] - $oldlen );
            $filter_stdout = 1; # I.e. suppress the lot
         }
         else {
            print STDOUT $content;
         }
      }
      elsif( $filter_stdout eq "body" ) {
         $stdout .= $content;
         if( $stdout =~ m/\r\n\r\n/ ) {
            print STDOUT substr( $stdout, $+[0] );
            $filter_stdout = undef;
         }
      }
   }
   elsif( $type == FCGI_STDERR ) {
      print STDERR $content;
   }
   elsif( $type == FCGI_END_REQUEST ) {
      my ( $app_status, $protocol_status ) = parse_end_request_body( $content );
      exit $app_status;
   }
   else {
      die "Unrecognised FastCGI request type $type\n";
   }
}