# $Id: Daemon.pm,v 1.13 1996/11/13 13:22:21 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, ">\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 class are HTTP/1.1 servers that listens on a socket for incoming requests. The I is a sub-class of I, 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 class which is another I subclass. Calling the get_request() method on this object will read data from the client and return an I object reference. This HTTP daemon does not fork(2) for you. Your application, i.e. the user of the I 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 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 base class. =over 4 =cut use vars qw($VERSION @ISA $PROTO); $VERSION = sprintf("%d.%02d", q$Revision: 1.13 $ =~ /(\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 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 Same as I but will return an I reference. 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 $sock = $self->SUPER::accept(@_); if ($sock) { $sock = bless $sock, "HTTP::Daemon::ClientConn"; ${*$sock}{'httpd_daemon'} = $self; } $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 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 is also a I subclass. Instances of this class are returned by the accept() method of the I. The following additional methods are provided: =over 4 =item $c->get_request Will read data from the client and turn it into a I object which is then returned. Will return undef if reading of the request failed. If it fails, then the I object ($c) should be discarded. The $c->get_request method support HTTP/1.1 content bodies, including I transfer encoding with footer and I 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 == 0; # 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 == 0; $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 == 0; } } $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 == 0; } 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 == 0; } $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 == 0; $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 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*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 <$status $mess

$status $mess

$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 <= 0; $cnt += $n; print $self $buf; } close($file) if $opened; $cnt; } =item $c->daemon Return a reference to the corresponding I object. =cut sub daemon { my $self = shift; ${*$self}{'httpd_daemon'}; } =back =head1 SEE ALSO L, L =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;