The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package CPAN::Mirror::Server::HTTP;
{
  $CPAN::Mirror::Server::HTTP::VERSION = '0.04';
}

#ABSTRACT: Simple HTTP server for serving a CPAN mirror

use strict;
use warnings;
use Cwd ();
use Pod::Usage;
use HTTP::Daemon;
use HTTP::Status;
use HTTP::Response;
use HTML::Tiny;
use File::Spec;
use MIME::Base64 qw[decode_base64];
use Number::Bytes::Human qw[format_bytes];
use POSIX qw[strftime :sys_wait_h];
use Getopt::Long;

my %icons_encoded = (
back =>
'R0lGODlhFAAWAMIAAP///8z//5mZmWZmZjMzMwAAAAAAAAAAACH+TlRoaXMgYXJ0IGlzIGluIHRo
ZSBwdWJsaWMgZG9tYWluLiBLZXZpbiBIdWdoZXMsIGtldmluaEBlaXQuY29tLCBTZXB0ZW1iZXIg
MTk5NQAh+QQBAAABACwAAAAAFAAWAAADSxi63P4jEPJqEDNTu6LO3PVpnDdOFnaCkHQGBTcqRRxu
WG0v+5LrNUZQ8QPqeMakkaZsFihOpyDajMCoOoJAGNVWkt7QVfzokc+LBAA7',
blank =>
'R0lGODlhFAAWAKEAAP///8z//wAAAAAAACH+TlRoaXMgYXJ0IGlzIGluIHRoZSBwdWJsaWMgZG9t
YWluLiBLZXZpbiBIdWdoZXMsIGtldmluaEBlaXQuY29tLCBTZXB0ZW1iZXIgMTk5NQAh+QQBAAAB
ACwAAAAAFAAWAAACE4yPqcvtD6OctNqLs968+w+GSQEAOw==',
compressed =>
'R0lGODlhFAAWAOcAAP//////zP//mf//Zv//M///AP/M///MzP/Mmf/MZv/MM//MAP+Z//+ZzP+Z
mf+ZZv+ZM/+ZAP9m//9mzP9mmf9mZv9mM/9mAP8z//8zzP8zmf8zZv8zM/8zAP8A//8AzP8Amf8A
Zv8AM/8AAMz//8z/zMz/mcz/Zsz/M8z/AMzM/8zMzMzMmczMZszMM8zMAMyZ/8yZzMyZmcyZZsyZ
M8yZAMxm/8xmzMxmmcxmZsxmM8xmAMwz/8wzzMwzmcwzZswzM8wzAMwA/8wAzMwAmcwAZswAM8wA
AJn//5n/zJn/mZn/Zpn/M5n/AJnM/5nMzJnMmZnMZpnMM5nMAJmZ/5mZzJmZmZmZZpmZM5mZAJlm
/5lmzJlmmZlmZplmM5lmAJkz/5kzzJkzmZkzZpkzM5kzAJkA/5kAzJkAmZkAZpkAM5kAAGb//2b/
zGb/mWb/Zmb/M2b/AGbM/2bMzGbMmWbMZmbMM2bMAGaZ/2aZzGaZmWaZZmaZM2aZAGZm/2ZmzGZm
mWZmZmZmM2ZmAGYz/2YzzGYzmWYzZmYzM2YzAGYA/2YAzGYAmWYAZmYAM2YAADP//zP/zDP/mTP/
ZjP/MzP/ADPM/zPMzDPMmTPMZjPMMzPMADOZ/zOZzDOZmTOZZjOZMzOZADNm/zNmzDNmmTNmZjNm
MzNmADMz/zMzzDMzmTMzZjMzMzMzADMA/zMAzDMAmTMAZjMAMzMAAAD//wD/zAD/mQD/ZgD/MwD/
AADM/wDMzADMmQDMZgDMMwDMAACZ/wCZzACZmQCZZgCZMwCZAABm/wBmzABmmQBmZgBmMwBmAAAz
/wAzzAAzmQAzZgAzMwAzAAAA/wAAzAAAmQAAZgAAM+4AAN0AALsAAKoAAIgAAHcAAFUAAEQAACIA
ABEAAADuAADdAAC7AACqAACIAAB3AABVAABEAAAiAAARAAAA7gAA3QAAuwAAqgAAiAAAdwAAVQAA
RAAAIgAAEe7u7t3d3bu7u6qqqoiIiHd3d1VVVURERCIiIhEREQAAACH+TlRoaXMgYXJ0IGlzIGlu
IHRoZSBwdWJsaWMgZG9tYWluLiBLZXZpbiBIdWdoZXMsIGtldmluaEBlaXQuY29tLCBTZXB0ZW1i
ZXIgMTk5NQAh+QQBAAAkACwAAAAAFAAWAAAImQBJCCTBqmDBgQgTDmQFAABDVgojEmzI0KHEhBUr
WrwoMGNDihwnAvjHiqRJjhX/qVz5D+VHAFZiWmmZ8BGHji9hxqTJ4ZFAmzc1vpxJgkPPn0Y5CP04
M6lPEkCN5mxoJelRqFY5TM36NGrPqV67Op0KM6rYnkup/gMq1mdamC1tdn36lijUpwjr0pSoFyUr
mTJLhiTBkqXCgAA7',
folder =>
'R0lGODlhFAAWAMIAAP/////Mmcz//5lmMzMzMwAAAAAAAAAAACH+TlRoaXMgYXJ0IGlzIGluIHRo
ZSBwdWJsaWMgZG9tYWluLiBLZXZpbiBIdWdoZXMsIGtldmluaEBlaXQuY29tLCBTZXB0ZW1iZXIg
MTk5NQAh+QQBAAACACwAAAAAFAAWAAADVCi63P4wyklZufjOErrvRcR9ZKYpxUB6aokGQyzHKxyO
9RoTV54PPJyPBewNSUXhcWc8soJOIjTaSVJhVphWxd3CeILUbDwmgMPmtHrNIyxM8Iw7AQA7',
text =>
'R0lGODlhFAAWAMIAAP///8z//5mZmTMzMwAAAAAAAAAAAAAAACH+TlRoaXMgYXJ0IGlzIGluIHRo
ZSBwdWJsaWMgZG9tYWluLiBLZXZpbiBIdWdoZXMsIGtldmluaEBlaXQuY29tLCBTZXB0ZW1iZXIg
MTk5NQAh+QQBAAABACwAAAAAFAAWAAADWDi6vPEwDECrnSO+aTvPEddVIriN1wVxROtSxBDPJwq7
bo23luALhJqt8gtKbrsXBSgcEo2spBLAPDp7UKT02bxWRdrp94rtbpdZMrrr/A5+8LhPFpHajQkA
Ow==',
unknown =>
'R0lGODlhFAAWAMIAAP///8z//5mZmTMzMwAAAAAAAAAAAAAAACH+TlRoaXMgYXJ0IGlzIGluIHRo
ZSBwdWJsaWMgZG9tYWluLiBLZXZpbiBIdWdoZXMsIGtldmluaEBlaXQuY29tLCBTZXB0ZW1iZXIg
MTk5NQAh+QQBAAABACwAAAAAFAAWAAADaDi6vPEwDECrnSO+aTvPEQcIAmGaIrhR5XmKgMq1LkoM
N7ECrjDWp52r0iPpJJ0KjUAq7SxLE+sI+9V8vycFiM0iLb2O80s8JcfVJJTaGYrZYPNby5Ov6Wol
PD+XDJqAgSQ4EUCGQQEJADs=',
);

my %icons = map { ( $_ => decode_base64( $icons_encoded{$_} ) ) }
              keys %icons_encoded;

my $index = 'index.html';

sub run {
  my $root = Cwd::getcwd();
  my $port = '8080';

  GetOptions(
    "root=s", \$root,
    "port=i", \$port,
  ) or pod2usage(2);

  local $SIG{CHLD};

  sub _REAPER {
    my $child;
    while (($child = waitpid(-1,WNOHANG)) > 0) {}
    $SIG{CHLD} = \&_REAPER; # still loathe SysV
  };

  $SIG{CHLD} = \&_REAPER;

  my $httpd = HTTP::Daemon->new( LocalPort => $port )
                or die "$!\n";

  while ( 1 ) {
    my $conn = $httpd->accept;
    next unless $conn;
    my $child = fork();
    unless ( defined $child ) {
      die "Cannot fork child: $!\n";
    }
    if ( $child == 0 ) {
      _handle_request( $conn, $root );
      exit(0);
    }
    $conn->close();
  }

}

sub _handle_request {
  my $conn = shift;
  my $root = shift;
  REQ: while (my $req = $conn->get_request) {
    if ($req->method eq 'GET' or $req->method eq 'HEAD') {
      # Special case /icons
      if ( my ($icon) = $req->uri->path =~ m#^/icons/(back|blank|compressed|folder|unknown)\.gif$# ) {
        my $resp = _gen_icon( $icon );
        $conn->send_response( $resp );
        next REQ;
      }
      my @path = $req->uri->path_segments;
      my $path = File::Spec->catfile( $root, @path );
      if ( -d $path and $req->uri->path !~ m#/$# ) {
        my $resp = _gen_301( $req->uri );
        $conn->send_response( $resp );
        next REQ;
      }
      if ( -d $path and -e File::Spec->catfile( $path, $index ) ) {
        $path = File::Spec->catfile( $path, $index );
      }
      if ( -d $path ) {
        my $resp = _gen_dir( $req->uri, $path );
        $conn->send_response( $resp );
        next REQ;
      }
      unless ( -e $path ) {
        $conn->send_error(RC_NOT_FOUND);
        next REQ;
      }
      $conn->send_file_response( $path );
    }
    else {
      $conn->send_error(RC_FORBIDDEN)
    }
  }
}

sub _gen_dir {
  my $uri  = shift;
  my $path = shift;
  my $resp = HTTP::Response->new( 200 );
  my %dir;

  {
    opendir my $DIR, $path or die "$!\n";
  
    $dir{ $_ } = [ ( stat( File::Spec->catfile( $path, $_ ) ) )[7,9],
                   ( -d File::Spec->catfile( $path, $_ ) ? 1 : 0 ),
                 ] for grep { !/^\./ } readdir $DIR;
  }

  my $h = HTML::Tiny->new;

  my @data;
  foreach my $item ( sort keys %dir ) {
    my $data = $dir{$item};
    push @data, [ 
      $h->td( { valign => 'top' }, 
        [ $h->img({ src => '/icons/' . _guess_type( $data->[2], $item ), 
            alt => ( $data->[2] ? '[DIR]' : '[   ]' ) }) ],
        [ $h->a( { href => ( $data->[2] ? "$item/" : $item ) }, $item ) ],
        { align => 'right' },
        strftime("%d-%b-%Y %H:%M",localtime($data->[1])),
        { align => 'right' },
        format_bytes( $data->[0] ),
      ),
    ];
  }

  my $parent;

  {
    my @segs = split m#/#, $uri->path;
    if ( scalar @segs ) {
      pop @segs;
      if ( grep { $_ } @segs ) {
        $parent = join('/', @segs);
      }
      $parent .= '/';
    }
  }

  unshift @data, 
    [ $h->td( { valign => 'top' }, 
      [ $h->img({ src => '/icons/back.gif', alt => '[DIR]' }) ], 
      [ $h->a( { href => $parent }, 'Parent Directory' ) ],
      ' ',
      '  - ', )
    ]
    if $parent;

  my $html = $h->html(
    [
      $h->head( $h->title( 'Index of ' . $uri->path ) ),
      $h->body( 
        [
          $h->h1( 'Index of ' . $uri->path ),
          $h->table(
            [
              $h->tr(
                [ $h->th( [ $h->img({ src => '/icons/blank.gif', alt => '[ICO]' }) ], 
                                'Name', 'Last modified', 'Size' ) ],
                [ $h->th( { colspan => 4 }, [ $h->hr() ] ) ],
                @data,
                [ $h->th( { colspan => 4 }, [ $h->hr() ] ) ],
              ),
            ],
          ),
        ],
      ),
    ],
  );

  $resp->header( 'Content-Type', 'text/html' );
  $resp->content( $html );
  {
    use bytes;
    $resp->header( 'Content-Length', length $resp->content );
  }
  return $resp;
}

sub _gen_icon {
  my $icon = shift;
  my $resp = HTTP::Response->new( 200 );
  $resp->header( 'Content-Type', 'image/gif' );
  $resp->content( $icons{ $icon } );
  {
    use bytes;
    $resp->header( 'Content-Length', length $resp->content );
  }
  return $resp;
}

sub _guess_type {
  my $flag = shift;
  return 'folder.gif' if $flag;
  my $item = shift;
  return 'compressed.gif' if $item =~ m!(\.tar\.gz|\.tar\.bz2|\.tgz|\.zip)$!i;
  return 'unknown.gif';
}

sub _gen_301 {
  my $uri = shift;
  my $resp = HTTP::Response->new( 301 );
  my $path = $uri->path . '/';
  my $h = HTML::Tiny->new();
  $resp->header( 'Location' => $path );
  $resp->header( 'Content-Type', 'text/html' );
  $resp->content(
     $h->html( 
        [ $h->head( $h->title( '301' ) ), 
          $h->body( 
            [ $h->h1('Moved Permanently'), $h->p( [ 'The document has moved ', $h->a( { href => $path }, 'here' ) ] ), ]
          ),
        ] ),
  );
  {
    use bytes;
    $resp->header( 'Content-Length', length $resp->content );
  }
  return $resp;
}

q[CPAN Mirror on the wall, who's the fairest of them all?];


__END__
=pod

=head1 NAME

CPAN::Mirror::Server::HTTP - Simple HTTP server for serving a CPAN mirror

=head1 VERSION

version 0.04

=head1 SYNPOSIS

  #!/usr/bin/perl
  use strict;
  use warnings;
  use CPAN::Mirror::Server::HTTP;
  CPAN::Mirror::Server::HTTP->run();

=head2 C<run>

This method is called by L<cpanmirrorhttpd> to do all the work.

=head1 AUTHOR

Chris Williams <chris@bingosnet.co.uk>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2012 by Chris Williams.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut