home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Acorn User 10
/
AU_CD10.iso
/
Updates
/
Perl
/
Perl_Libs
/
site_perl
/
HTTP
/
Daemon.pm
< prev
next >
Wrap
Text File
|
1997-11-26
|
13KB
|
582 lines
# $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;