package Catalyst::Engine::HTTP; use Moose; extends 'Catalyst::Engine::CGI'; use Data::Dump qw(dump); use Errno 'EWOULDBLOCK'; use HTTP::Date (); use HTTP::Headers; use HTTP::Status; use Socket; use IO::Socket::INET (); use IO::Select (); use constant CHUNKSIZE => 64 * 1024; use constant DEBUG => $ENV{CATALYST_HTTP_DEBUG} || 0; use namespace::clean -except => 'meta'; has options => ( is => 'rw' ); has _keepalive => ( is => 'rw', predicate => '_is_keepalive', clearer => '_clear_keepalive' ); has _write_error => ( is => 'rw', predicate => '_has_write_error' ); # Refactoring note - could/should Eliminate all instances of $self->{inputbuf}, # which I haven't touched as it is used as an lvalue in a lot of places, and I guess # doing it differently could be expensive.. Feel free to refactor and NYTProf :) =head1 NAME Catalyst::Engine::HTTP - Catalyst HTTP Engine =head1 SYNOPSIS A script using the Catalyst::Engine::HTTP module might look like: #!/usr/bin/perl -w BEGIN { $ENV{CATALYST_ENGINE} = 'HTTP' } use strict; use lib '/path/to/MyApp/lib'; use MyApp; MyApp->run; =head1 DESCRIPTION This is the Catalyst engine specialized for development and testing. =head1 METHODS =head2 $self->finalize_headers($c) =cut sub finalize_headers { my ( $self, $c ) = @_; my $protocol = $c->request->protocol; my $status = $c->response->status; my $message = status_message($status); my $res_headers = $c->response->headers; my @headers; push @headers, "$protocol $status $message"; $res_headers->header( Date => HTTP::Date::time2str(time) ); $res_headers->header( Status => $status ); # Should we keep the connection open? my $connection = $c->request->header('Connection'); if ( $self->options->{keepalive} && $connection && $connection =~ /^keep-alive$/i ) { $res_headers->header( Connection => 'keep-alive' ); $self->_keepalive(1); } else { $res_headers->header( Connection => 'close' ); } push @headers, $res_headers->as_string("\x0D\x0A"); # Buffer the headers so they are sent with the first write() call # This reduces the number of TCP packets we are sending $self->_header_buf( join("\x0D\x0A", @headers, '') ); } =head2 $self->finalize_read($c) =cut before finalize_read => sub { # Never ever remove this, it would result in random length output # streams if STDIN eq STDOUT (like in the HTTP engine) *STDIN->blocking(1); }; =head2 $self->prepare_read($c) =cut before prepare_read => sub { # Set the input handle to non-blocking *STDIN->blocking(0); }; =head2 $self->read_chunk($c, $buffer, $length) =cut sub read_chunk { my $self = shift; my $c = shift; # If we have any remaining data in the input buffer, send it back first if ( $_[0] = delete $self->{inputbuf} ) { my $read = length( $_[0] ); DEBUG && warn "read_chunk: Read $read bytes from previous input buffer\n"; return $read; } # support for non-blocking IO my $rin = ''; vec( $rin, *STDIN->fileno, 1 ) = 1; READ: { select( $rin, undef, undef, undef ); my $rc = *STDIN->sysread(@_); if ( defined $rc ) { DEBUG && warn "read_chunk: Read $rc bytes from socket\n"; return $rc; } else { next READ if $! == EWOULDBLOCK; return; } } } =head2 $self->write($c, $buffer) Writes the buffer to the client. =cut around write => sub { my $orig = shift; my ( $self, $c, $buffer ) = @_; # Avoid 'print() on closed filehandle Remote' warnings when using IE return unless *STDOUT->opened(); # Prepend the headers if they have not yet been sent if ( $self->_has_header_buf ) { $self->_warn_on_write_error( $self->$orig($c, $self->_clear_header_buf) ); } $self->_warn_on_write_error($self->$orig($c, $buffer)); }; sub _warn_on_write_error { my ($self, $ret) = @_; if ( !defined $ret ) { $self->_write_error($!); DEBUG && warn "write: Failed to write response ($!)\n"; } else { DEBUG && warn "write: Wrote response ($ret bytes)\n"; } return $ret; } =head2 run =cut # A very very simple HTTP server that initializes a CGI environment sub run { my ( $self, $class, $port, $host, $options ) = @_; $options ||= {}; $self->options($options); if ($options->{background}) { my $child = fork; die "Can't fork: $!" unless defined($child); return $child if $child; } my $restart = 0; local $SIG{CHLD} = 'IGNORE'; my $allowed = $options->{allowed} || { '127.0.0.1' => '255.255.255.255' }; my $addr = $host ? inet_aton($host) : INADDR_ANY; if ( $addr eq INADDR_ANY ) { require Sys::Hostname; $host = lc Sys::Hostname::hostname(); } else { $host = gethostbyaddr( $addr, AF_INET ) || inet_ntoa($addr); } # Handle requests # Setup socket my $daemon = IO::Socket::INET->new( Listen => SOMAXCONN, LocalAddr => inet_ntoa($addr), LocalPort => $port, Proto => 'tcp', ReuseAddr => 1, Type => SOCK_STREAM, ) or die "Couldn't create daemon: $@"; $port = $daemon->sockport(); my $url = "http://$host"; $url .= ":$port" unless $port == 80; print "You can connect to your server at $url\n"; if ($options->{background}) { open STDIN, "+&STDIN" or die $!; open STDERR, ">&STDIN" or die $!; if ( $^O !~ /MSWin32/ ) { require POSIX; POSIX::setsid() or die "Can't start a new session: $!"; } } if (my $pidfile = $options->{pidfile}) { if (! open PIDFILE, "> $pidfile") { warn("Cannot open: $pidfile: $!"); } print PIDFILE "$$\n"; close PIDFILE; } my $pid = undef; # Ignore broken pipes as an HTTP server should local $SIG{PIPE} = 'IGNORE'; # Restart on HUP local $SIG{HUP} = sub { $restart = 1; warn "Restarting server on SIGHUP...\n"; }; LISTEN: while ( !$restart ) { while ( accept( Remote, $daemon ) ) { DEBUG && warn "New connection\n"; select Remote; Remote->blocking(1); # Read until we see all headers $self->{inputbuf} = ''; if ( !$self->_read_headers ) { # Error reading, give up close Remote; next LISTEN; } my ( $method, $uri, $protocol ) = $self->_parse_request_line; DEBUG && warn "Parsed request: $method $uri $protocol\n"; next unless $method; unless ( uc($method) eq 'RESTART' ) { # Fork if ( $options->{fork} ) { if ( $pid = fork ) { DEBUG && warn "Forked child $pid\n"; next; } } $self->_handler( $class, $port, $method, $uri, $protocol ); if ( $self->_has_write_error ) { close Remote; if ( !defined $pid ) { next LISTEN; } } if ( defined $pid ) { # Child process, close connection and exit DEBUG && warn "Child process exiting\n"; $daemon->close; exit; } } else { my $sockdata = $self->_socket_data( \*Remote ); my $ipaddr = _inet_addr( $sockdata->{peeraddr} ); my $ready = 0; foreach my $ip ( keys %$allowed ) { my $mask = $allowed->{$ip}; $ready = ( $ipaddr & _inet_addr($mask) ) == _inet_addr($ip); last if $ready; } if ($ready) { $restart = 1; last; } } } continue { close Remote; } } $daemon->close; DEBUG && warn "Shutting down\n"; if ($restart) { $SIG{CHLD} = 'DEFAULT'; wait; ### if the standalone server was invoked with perl -I .. we will loose ### those include dirs upon re-exec. So add them to PERL5LIB, so they ### are available again for the exec'ed process --kane use Config; $ENV{PERL5LIB} .= join $Config{path_sep}, @INC; exec $^X, $0, @{ $options->{argv} }; } exit; } sub _handler { my ( $self, $class, $port, $method, $uri, $protocol ) = @_; local *STDIN = \*Remote; local *STDOUT = \*Remote; # We better be careful and just use 1.0 $protocol = '1.0'; my $sockdata = $self->_socket_data( \*Remote ); my %copy_of_env = %ENV; my $sel = IO::Select->new; $sel->add( \*STDIN ); REQUEST: while (1) { my ( $path, $query_string ) = split /\?/, $uri, 2; # URI is not the same as path. Remove scheme, domain name and port from it $path =~ s{^https?://[^/?#]+}{}; # Initialize CGI environment local %ENV = ( PATH_INFO => $path || '', QUERY_STRING => $query_string || '', REMOTE_ADDR => $sockdata->{peeraddr}, REQUEST_METHOD => $method || '', SERVER_NAME => $sockdata->{localname}, SERVER_PORT => $port, SERVER_PROTOCOL => "HTTP/$protocol", %copy_of_env, ); # Parse headers if ( $protocol >= 1 ) { $self->_parse_headers; } # Pass flow control to Catalyst { # FIXME: don't ignore SIGCHLD while handling requests so system() # et al. work within actions. it might be a little risky to do that # this far out, but then again it's only the dev server anyway. local $SIG{CHLD} = 'DEFAULT'; $class->handle_request( env => \%ENV ); } DEBUG && warn "Request done\n"; # Allow keepalive requests, this is a hack but we'll support it until # the next major release. if ( $self->_is_keepalive ) { $self->_clear_keepalive; DEBUG && warn "Reusing previous connection for keep-alive request\n"; if ( $sel->can_read(1) ) { if ( !$self->_read_headers ) { # Error reading, give up last REQUEST; } ( $method, $uri, $protocol ) = $self->_parse_request_line; DEBUG && warn "Parsed request: $method $uri $protocol\n"; # Force HTTP/1.0 $protocol = '1.0'; next REQUEST; } DEBUG && warn "No keep-alive request within 1 second\n"; } last REQUEST; } DEBUG && warn "Closing connection\n"; close Remote; } sub _read_headers { my $self = shift; while (1) { my $read = sysread Remote, my $buf, CHUNKSIZE; if ( !defined $read ) { next if $! == EWOULDBLOCK; DEBUG && warn "Error reading headers: $!\n"; return; } elsif ( $read == 0 ) { DEBUG && warn "EOF\n"; return; } DEBUG && warn "Read $read bytes\n"; $self->{inputbuf} .= $buf; last if $self->{inputbuf} =~ /(\x0D\x0A?\x0D\x0A?|\x0A\x0D?\x0A\x0D?)/s; } return 1; } sub _parse_request_line { my $self = shift; # Parse request line # Leading CRLF sometimes sent by buggy IE versions if ( $self->{inputbuf} !~ s/^(?:\x0D\x0A)?(\w+)[ \t]+(\S+)(?:[ \t]+(HTTP\/\d+\.\d+))?[^\012]*\012// ) { return (); } my $method = $1; my $uri = $2; my $proto = $3 || 'HTTP/0.9'; return ( $method, $uri, $proto ); } sub _parse_headers { my $self = shift; # Copy the buffer for header parsing, and remove the header block # from the content buffer. my $buf = $self->{inputbuf}; $self->{inputbuf} =~ s/.*?(\x0D\x0A?\x0D\x0A?|\x0A\x0D?\x0A\x0D?)//s; # Parse headers my $headers = HTTP::Headers->new; my ($key, $val); HEADER: while ( $buf =~ s/^([^\012]*)\012// ) { $_ = $1; s/\015$//; if ( /^([\w\-~]+)\s*:\s*(.*)/ ) { $headers->push_header( $key, $val ) if $key; ($key, $val) = ($1, $2); } elsif ( /^\s+(.*)/ ) { $val .= " $1"; } else { last HEADER; } } $headers->push_header( $key, $val ) if $key; DEBUG && warn "Parsed headers: " . dump($headers) . "\n"; # Convert headers into ENV vars $headers->scan( sub { my ( $key, $val ) = @_; $key = uc $key; $key = 'COOKIE' if $key eq 'COOKIES'; $key =~ tr/-/_/; $key = 'HTTP_' . $key unless $key =~ m/\A(?:CONTENT_(?:LENGTH|TYPE)|COOKIE)\z/; if ( exists $ENV{$key} ) { $ENV{$key} .= ", $val"; } else { $ENV{$key} = $val; } } ); } sub _socket_data { my ( $self, $handle ) = @_; my $remote_sockaddr = getpeername($handle); my ( undef, $iaddr ) = $remote_sockaddr ? sockaddr_in($remote_sockaddr) : (undef, undef); my $local_sockaddr = getsockname($handle); my ( undef, $localiaddr ) = sockaddr_in($local_sockaddr); # This mess is necessary to keep IE from crashing the server my $data = { peeraddr => $iaddr ? ( inet_ntoa($iaddr) || '127.0.0.1' ) : '127.0.0.1', localname => gethostbyaddr( $localiaddr, AF_INET ) || 'localhost', localaddr => inet_ntoa($localiaddr) || '127.0.0.1', }; return $data; } sub _inet_addr { unpack "N*", inet_aton( $_[0] ) } =head2 options Options hash passed to the http engine to control things like if keepalive is supported. =head1 SEE ALSO L, L =head1 AUTHORS Catalyst Contributors, see Catalyst.pm =head1 THANKS Many parts are ripped out of C by Jesse Vincent. =head1 COPYRIGHT This library is free software. You can redistribute it and/or modify it under the same terms as Perl itself. =cut 1;