package LWP::Conn::FILE; use strict; require LWP::Version; # Ideally, we should make this implementation shareable with # HTTP::Daemon. use HTTP::Date qw(time2str str2time); use LWP::MediaTypes qw(guess_media_type); # Test to see if the system has getpwuid and getgrgid. eval { my $tmp = getpwuid($<); }; my $has_getpwuid = ! $@; eval { my $tmp = getgrgid($(); }; my $has_getgrgid = ! $@; sub new { my($class, %cnf) = @_; my $mgr = delete $cnf{ManagedBy} || Carp::croak("'ManagedBy' is mandatory"); # don't care about other configuration parameters yet # process all request in the queue while (my $req = $mgr->get_request(__PACKAGE__)) { my $url = $req->url; my $host = $url->host; if ($host && $host ne "localhost") { # generate redirect to ftp serveer my $loc = $url->as_string; $loc =~ s/^\w+:/ftp:/; $req->give_response(301, "Use ftp instead", {Location => $loc}); next; } my $method = uc($req->method); my $path = $url->file; if ($method eq "HEAD" || $method eq "GET") { get($req, $path, $method eq "GET"); } elsif ($method eq "PUT") { if ($req->header("Content-Range")) { $req->give_response(506, "Don't handle partial content updates yet"); next; } put($req, $path); } elsif ($method eq "DELETE") { # XXX must really handle If-XXX headers if (unlink($path)) { $req->give_response(204, "OK"); } else { $req->give_response(errno_status(), "$!"); } } elsif ($method eq "TRACE") { # Just for fun! my $res = $req->new_response(200, "OK"); $res->date(time); $res->server($LWP::Version::PRODUCT_TOKEN); $res->content_type("message/http"); $res->content($req->as_string); $req->response_done($res); } else { $req->give_response(405, "Bad method '$method'"); } } undef; # not really a connection } sub get { my($req, $path, $send_content) = @_; local(*DIR); if (opendir(DIR, $path)) { dir($req, $path, \*DIR, $send_content); closedir(DIR); return; } local(*FILE); if (sysopen(FILE, $path, 0)) { my $res = $req->new_response(200, "OK"); my $now = time; $res->date($now); $res->server($LWP::Version::PRODUCT_TOKEN); my($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$filesize, $atime,$mtime,$ctime,$blksize,$blocks) = stat(FILE); my $uname = ($has_getpwuid ? getpwuid($uid) : undef) || $uid; my $gname = ($has_getgrgid ? getgrgid($gid) : undef) || $gid; # far more than you ever wanted to know $res->header("INode" => sprintf("[%04x]:%d", $dev, $ino)) if $ino; $res->header("Owner" => $uname); $res->header("Group" => $gname); $res->header("Content-Length" => $filesize); $res->header("Blocks-Allocated" => $blocks); $res->header("Last-Modified" => time2str($mtime)); $res->header("Last-Accessed" => time2str($atime)); $res->header("Status-Modified" => time2str($ctime)); $res->header("Content-Location" => "file:$path"); # XXX absolutize guess_media_type($path, $res); # We use the same algoritm as Apache to generate an etag. my $etag = sprintf qq("%x-%x-%x"), $ino, $filesize, $mtime; $etag = "W/$etag" if $now - $mtime < 2; $res->header("ETag" => $etag); # Check various If-XXX headers if (my $ius = $req->header("If-Unmodified-Since")) { $ius = str2time($ius); if ($ius && $mtime > $ius) { $res->code(412); # PRECONDITION_FAILED $res->message("Resouce modified"); close(FILE); $req->response_done($res); return; } } if (my @im = $req->header("If-Match")) { my $im = join(", ", @im); my $orig_im = $im; if ($im ne "*") { my $match = 0; while (length($im)) { if ($im =~ s|^\s*(W/)?(\"[^\"]*\")\s*,?\s*||) { next if $1; # must use strong comparison if ($2 eq $etag) { $match++; last; } } else { last; # illegal value } } #$res->header("X-Unprocessed-If-Match", $im) if $im; unless ($match) { $res->code(412); # PRECONDITION_FAILED $res->message("No match for ETag $orig_im"); close(FILE); $req->response_done($res); return; } } } my $skip_if_modified; if (my @inm = $req->header("If-None-Match")) { my $inm = join(", ", @inm); my $match; my $etag2 = $etag; $etag2 =~ s,^W/,,; $match = "*" if $inm eq "*"; while (!$match && length($inm)) { if ($inm =~ s|^\s*(W/?(\"[^\"]*\"))\s*,?\s*||) { $match = $1 if $2 eq $etag; } else { last; # illegal value } } if ($match) { #$res->code(412); # PRECONDITION_FAILED $res->code(304); # NOT_MODIFIED $res->message("ETag match for $match"); close(FILE); $req->response_done($res); return; } $skip_if_modified++; } if (!$skip_if_modified && (my $ims = $req->header("If-Modified-Since"))) { $ims = str2time($ims); if ($ims && $mtime <= $ims) { $res->code(304); $res->message("Not modified"); close(FILE); $req->response_done($res); return; } } # XXX Implement the Range header??? if ($send_content) { my $buf; while (my $n = sysread(FILE, $buf, 1024)) { eval { $req->response_data($buf, $res); }; if ($@) { chomp($@); $res->header('X-Died' => $@); last; } } } close(FILE); $req->response_done($res) } else { $req->give_response(errno_status(), "$!"); } } sub dir { my($req, $path, $dir, $send_content) = @_; $req->give_response(501, "Directory reading", $path); #NYI } sub put { my($req, $path) = @_; $req->give_response(501, "File updating", $path); #NYI } sub errno_status { if ($! =~ /No such file/) { return 404; } else { return 403; } } 1;