# $Id: Daemon.pm,v 1.15 1997/11/26 10:44:08 aas Exp $
#

use strict;

package HTTP::Daemon;

=head1 NAME

HTTP::Daemon - a simple http server class

=head1 SYNOPSIS

  use HTTP::Daemon;
  use HTTP::Status;

  $d = new HTTP::Daemon;
  print "Please contact me at: <URL:", $d->url, ">\n";
  while ($c = $d->accept) {
      $r = $c->get_request;
      if ($r) {
	  if ($r->method eq 'GET' and $r->url->path eq "/xyzzy") {
              # this is *not* recommened practice
	      $c->send_file_response("/etc/passwd");
	  } else {
	      $c->send_error(RC_FORBIDDEN)
	  }
      }
      $c = undef;  # close connection
  }

=head1 DESCRIPTION

Instances of the I<HTTP::Daemon> class are HTTP/1.1 servers that
listens on a socket for incoming requests. The I<HTTP::Daemon> is a
sub-class of I<IO::Socket::INET>, so you can do socket operations
directly on it.

The accept() method will return when a connection from a client is
available. The returned value will be a reference to a object of the
I<HTTP::Daemon::ClientConn> class which is another I<IO::Socket::INET>
subclass. Calling the get_request() method on this object will read
data from the client and return an I<HTTP::Request> object reference.

This HTTP daemon does not fork(2) for you.  Your application, i.e. the
user of the I<HTTP::Daemon> is reponsible for forking if that is
desirable.  Also note that the user is responsible for generating
responses that conforms to the HTTP/1.1 protocol.  The
I<HTTP::Daemon::ClientConn> provide some methods that make this easier.

=head1 METHODS

The following is a list of methods that are new (or enhanced) relative
to the I<IO::Socket::INET> base class.

=over 4

=cut


use vars qw($VERSION @ISA $PROTO);

$VERSION = sprintf("%d.%02d", q$Revision: 1.15 $ =~ /(\d+)\.(\d+)/);

use IO::Socket ();
@ISA=qw(IO::Socket::INET);

$PROTO = "HTTP/1.1";

=item $d = new HTTP::Daemon

The object constructor takes the same parameters as the
I<IO::Socket::INET> constructor.  It can also be called without
specifying any parameters. The daemon will then set up a listen queue
of 5 connections and allocate some random port number.  A server
that want to bind to some specific address on the standard HTTP port
will be constructed like this:

  $d = new HTTP::Daemon
        LocalAddr => 'www.someplace.com',
        LocalPort => 80;

=cut

sub new
{
    my($class, %args) = @_;
    $args{Listen} ||= 5;
    $args{Proto}  ||= 'tcp';
    my $self = $class->SUPER::new(%args);
    return undef unless $self;

    my $host = $args{LocalAddr};
    unless ($host) {
	require Sys::Hostname;
	$host = Sys::Hostname::hostname();
    }
    ${*$self}{'httpd_server_name'} = $host;
    $self;
}


=item $c = $d->accept([$pkg])

Same as I<IO::Socket::accept> but will return an
I<HTTP::Daemon::ClientConn> reference by default.  It will return
undef if you have specified a timeout and no connection is made within
that time.

=cut

sub accept
{
    my $self = shift;
    my $pkg = shift || "HTTP::Daemon::ClientConn";
    my $sock = $self->SUPER::accept($pkg);
    ${*$sock}{'httpd_daemon'} = $self if $sock;
    $sock;
}


=item $d->url

Returns a URL string that can be used to access the server root.

=cut

sub url
{
    my $self = shift;
    my $url = "http://";
    $url .= ${*$self}{'httpd_server_name'};
    my $port = $self->sockport;
    $url .= ":$port" if $port != 80;
    $url .= "/";
    $url;
}


=item $d->product_tokens

Returns the name that this server will use to identify itself.  This
is the string that is sent with the I<Server> response header.

=cut

sub product_tokens
{
    "libwww-perl-daemon/$HTTP::Daemon::VERSION";
}


package HTTP::Daemon::ClientConn;

use vars '@ISA';
use IO::Socket ();
@ISA=qw(IO::Socket::INET);

use HTTP::Request  ();
use HTTP::Response ();
use HTTP::Status;
use HTTP::Date qw(time2str);
use URI::URL qw(url);
use LWP::MediaTypes qw(guess_media_type);
use Carp ();

my $CRLF = "\015\012";   # "\r\n" is not portable

=back

The I<HTTP::Daemon::ClientConn> is also a I<IO::Socket::INET>
subclass. Instances of this class are returned by the accept() method
of the I<HTTP::Daemon>.  The following additional methods are
provided:

=over 4

=item $c->get_request

Will read data from the client and turn it into a I<HTTP::Request>
object which is then returned. Will return undef if reading of the
request failed.  If it fails, then the I<HTTP::Daemon::ClientConn>
object ($c) should be discarded.

The $c->get_request method support HTTP/1.1 content bodies, including
I<chunked> transfer encoding with footer and I<multipart/*> types.

=cut

sub get_request
{
    my $self = shift;
    my $buf = "";
    
    my $timeout = $ {*$self}{'io_socket_timeout'};
    my  $fdset = "";
    vec($fdset, $self->fileno,1) = 1;

  READ_HEADER:
    while (1) {
	if ($timeout) {
	    return undef unless select($fdset,undef,undef,$timeout);
	}
	my $n = sysread($self, $buf, 1024, length($buf));
	return undef if !$n;  # unexpected EOF
	if ($buf =~ /^\w+[^\n]+HTTP\/\d+\.\d+\015?\012/) {
	    last READ_HEADER if $buf =~ /(\015?\012){2}/;
	} elsif ($buf =~ /\012/) {
	    last READ_HEADER;  # HTTP/0.9 client
	}
    }
    $buf =~ s/^(\w+)\s+(\S+)(?:\s+(HTTP\/\d+\.\d+))?[^\012]*\012//;
    my $proto = $3 || "HTTP/0.9";
    ${*$self}{'httpd_client_proto'} = $proto;
    my $r = HTTP::Request->new($1, url($2, $self->daemon->url));
    $r->protocol($proto);

    my($key, $val);
  HEADER:
    while ($buf =~ s/^([^\012]*)\012//) {
	$_ = $1;
	s/\015$//;
	if (/^([\w\-]+)\s*:\s*(.*)/) {
	    $r->push_header($key, $val) if $key;
	    ($key, $val) = ($1, $2);
	} elsif (/^\s+(.*)/) {
	    $val .= " $1";
	} else {
	    last HEADER;
	}
    }
    $r->push_header($key, $val) if $key;

    my $te  = $r->header('Transfer-Encoding');
    my $ct  = $r->header('Content-Type');
    my $len = $r->header('Content-Length');

    if ($te && lc($te) eq 'chunked') {
	# Handle chunked transfer encoding
	my $body = "";
      CHUNK:
	while (1) {
	    if ($buf =~ s/^([^\012]*)\012//) {
		my $chunk_head = $1;
		$chunk_head =~ /^([0-9A-Fa-f]+)/;
		return undef unless length($1);
		my $size = hex($1);
		last CHUNK if $size == 0;

		my $missing = $size - length($buf);
		$missing += 2; # also read CRLF at chunk end
		$body .= $buf;
		$buf = "";
		# must read rest of chunk and append it to the $body
		while ($missing > 0) {
		    if ($timeout) {
			return undef unless select($fdset,undef,undef,$timeout);
		    }
		    my $n = sysread($self, $body, $missing, length($body));
		    return undef if !$n;
		    $missing -= $n;
		}
		substr($body, -2, 2) = ''; # remove CRLF at end

	    } else {
		# need more data in order to have a complete chunk header
		if ($timeout) {
		    return undef unless select($fdset,undef,undef,$timeout);
		}
		my $n = sysread($self, $buf, 2048, length($buf));
		return undef if !$n;
	    }
	}
	$r->content($body);

	# pretend it was a normal entity body
	$r->remove_header('Transfer-Encoding');
	$r->header('Content-Length', length($body));

	my($key, $val);
      FOOTER:
	while (1) {
	    if ($buf !~ /\012/) {
		# need at least one line to look at
		if ($timeout) {
		    return undef unless select($fdset,undef,undef,$timeout);
		}
		my $n = sysread($self, $buf, 2048, length($buf));
		return undef if !$n;
	    } else {
		$buf =~ s/^([^\012]*)\012//;
		$_ = $1;
		s/\015$//;
		last FOOTER if length($_) == 0;

		if (/^([\w\-]+)\s*:\s*(.*)/) {
		    $r->push_header($key, $val) if $key;
		    ($key, $val) = ($1, $2);
		} elsif (/^\s+(.*)/) {
		    $val .= " $1";
		} else {
		    return undef;  # bad syntax
		}
	    }
	}
	$r->push_header($key, $val) if $key;

    } elsif ($te) {
	# Unknown transfer encoding
	$self->send_error(501);
	return undef;

    } elsif ($ct && lc($ct) =~ m/^multipart\/\w+\s*;.*boundary\s*=\s*(\w+)/) {
	# Handle multipart content type
	my $boundary = "$CRLF--$1--$CRLF";
	while (index($buf, $boundary) < 0) {
	    # end marker not yet found
	    if ($timeout) {
		return undef unless select($fdset,undef,undef,$timeout);
	    }
	    my $n = sysread($self, $buf, 2048, length($buf));
	    return undef if !$n;
	}
	$r->content($buf);

    } elsif ($len) {
	# Plain body specified by "Content-Length"

	$len -= length($buf);
	while ($len > 0) {
	    if ($timeout) {
		return undef unless select($fdset,undef,undef,$timeout);
	    }
	    my $n = sysread($self, $buf, $len, length($buf));
	    return undef if !$n;
	    $len -= $n;
	}
	$r->content($buf);

    }

    $r;
}


=item $c->antique_client

Returns TRUE if the client speaks the HTTP/0.9 protocol, i.e. no
status code or headers should be returned.

=cut

sub antique_client
{
    my $self = shift;
    ${*$self}{'httpd_client_proto'} eq 'HTTP/0.9';
}


=item $c->send_status_line( [$code, [$mess, [$proto]]] )

Sends the status line back to the client.

=cut

sub send_status_line
{
    my($self, $status, $message, $proto) = @_;
    return if $self->antique_client;
    $status  ||= RC_OK;
    $message ||= status_message($status);
    $proto   ||= $HTTP::Daemon::PROTO;
    print $self "$proto $status $message$CRLF";
}


sub send_crlf
{
    my $self = shift;
    print $self $CRLF;
}


=item $c->send_basic_header( [$code, [$mess, [$proto]]] )

Sends the status line and the "Date:" and "Server:" headers back to
the client.

=cut

sub send_basic_header
{
    my $self = shift;
    return if $self->antique_client;
    $self->send_status_line(@_);
    print $self "Date: ", time2str(time), $CRLF;
    my $product = $self->daemon->product_tokens;
    print $self "Server: $product$CRLF" if $product;
}


=item $c->send_response( [$res] )

Takes a I<HTTP::Response> object as parameter and send it back to the
client as the response.

=cut

sub send_response
{
    my $self = shift;
    my $res = shift;
    if (!ref $res) {
	$res ||= RC_OK;
	$res = HTTP::Response->new($res, @_);
    }
    unless ($self->antique_client) {
	$self->send_basic_header($res->code, $res->message, $res->protocol);
	print $self $res->headers_as_string($CRLF);
	print $self $CRLF;  # separates headers and content
    }
    print $self $res->content;
}


=item $c->send_redirect( $loc, [$code, [$entity_body]] )

Sends a redirect response back to the client.  The location ($loc) can
be an absolute or a relative URL. The $code must be one the redirect
status codes, and it defaults to "301 Moved Permanently"

=cut

sub send_redirect
{
    my($self, $loc, $status, $content) = @_;
    $status ||= RC_MOVED_PERMANENTLY;
    Carp::croak("Status '$status' is not redirect") unless is_redirect($status);
    $self->send_basic_header($status);
    $loc = url($loc, $self->daemon->url) unless ref($loc);
    $loc = $loc->abs;
    print $self "Location: $loc$CRLF";
    if ($content) {
	my $ct = $content =~ /^\s*</ ? "text/html" : "text/plain";
	print $self "Content-Type: $ct$CRLF";
    }
    print $self $CRLF;
    print $self $content if $content;
}


=item $c->send_error( [$code, [$error_message]] )

Send an error response back to the client.  If the $code is missing a
"Bad Request" error is reported.  The $error_message is a string that
is incorporated in the body of the HTML entity body.

=cut

sub send_error
{
    my($self, $status, $error) = @_;
    $status ||= RC_BAD_REQUEST;
    Carp::croak("Status '$status' is not an error") unless is_error($status);
    my $mess = status_message($status);
    $error  ||= "";
    unless ($self->antique_client) {
        $self->send_basic_header($status);
        print $self "Content-Type: text/html$CRLF";
        print $self $CRLF;
    }
    print $self <<EOT;
<title>$status $mess</title>
<h1>$status $mess</h1>
$error
EOT
    $status;
}


=item $c->send_file_response($filename)

Send back a response with the specified $filename as content.  If the
file happen to be a directory we will generate a HTML index for it.

=cut

sub send_file_response
{
    my($self, $file) = @_;
    if (-d $file) {
	$self->send_dir($file);
    } elsif (-f _) {
	# plain file
	local(*F);
	sysopen(F, $file, 0) or 
	  return $self->send_error(RC_FORBIDDEN);
	my($ct,$ce) = guess_media_type($file);
	my($size,$mtime) = (stat _)[7,9];
	unless ($self->antique_client) {
	    $self->send_basic_header;
	    print $self "Content-Type: $ct$CRLF";
	    print $self "Content-Encoding: $ce$CRLF" if $ce;
	    print $self "Content-Length: $size$CRLF";
	    print $self "Last-Modified: ", time2str($mtime), "$CRLF";
	    print $self $CRLF;
	}
	$self->send_file(\*F);
	return RC_OK;
    } else {
	$self->send_error(RC_NOT_FOUND);
    }
}


sub send_dir
{
    my($self, $dir) = @_;
    $self->send_error(RC_NOT_FOUND) unless -d $dir;
    $self->send_error(RC_NOT_IMPLEMENTED);
}


=item $c->send_file($fd);

Copies the file back to the client.  The file can be a string (which
will be interpreted as a filename) or a reference to a glob.

=cut

sub send_file
{
    my($self, $file) = @_;
    my $opened = 0;
    if (!ref($file)) {
	local(*F);
	open(F, $file) || return undef;
	$file = \*F;
	$opened++;
    }
    my $cnt = 0;
    my $buf = "";
    my $n;
    while ($n = sysread($file, $buf, 8*1024)) {
	last if !$n;
	$cnt += $n;
	print $self $buf;
    }
    close($file) if $opened;
    $cnt;
}


=item $c->daemon

Return a reference to the corresponding I<HTTP::Daemon> object.

=cut

sub daemon
{
    my $self = shift;
    ${*$self}{'httpd_daemon'};
}

=back

=head1 SEE ALSO

L<IO::Socket>, L<Apache>

=head1 COPYRIGHT

Copyright 1996, Gisle Aas

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

=cut

1;
