Revision: 124 Author: jwalt Date: 2006-08-24 19:22:32 +0000 (Thu, 24 Aug 2006)
Log Message: ----------- - change uri_to_file to be more portable using File::Spec exclusively - chance uri_to_file to behave like apache regarding path_info and nonexistant files - add plugin typeless_uri to manage URIs without file extension - extend AxKit2::Test to provide a comfortable testing toolkit - add exhaustive tests for uri_to_file and typeless_uri Modified Paths: -------------- trunk/lib/AxKit2/Client.pm trunk/lib/AxKit2/Connection.pm trunk/lib/AxKit2/Test.pm trunk/plugins/uri_to_file Added Paths: ----------- trunk/plugins/typeless_uri trunk/t/ trunk/t/10uri_to_file.t trunk/t/11typeless_uri.t trunk/t/server1/ trunk/t/server1/foo/ trunk/t/server1/foo/index.html trunk/t/server1/index.html trunk/t/server1/multi/ trunk/t/server1/multi/index.html trunk/t/server1/multi.html Modified: trunk/lib/AxKit2/Client.pm =================================================================== --- trunk/lib/AxKit2/Client.pm 2006-08-24 18:11:37 UTC (rev 123) +++ trunk/lib/AxKit2/Client.pm 2006-08-24 19:22:32 UTC (rev 124) @@ -419,6 +419,7 @@ # stolen shamelessly from httpd-2.2.2/modules/http/http_protocol.c sub default_error_out { my ($self, $code, $extras) = @_; + $extras = '' unless defined $extras; $self->headers_out->code($code); Modified: trunk/lib/AxKit2/Connection.pm =================================================================== --- trunk/lib/AxKit2/Connection.pm 2006-08-24 18:11:37 UTC (rev 123) +++ trunk/lib/AxKit2/Connection.pm 2006-08-24 19:22:32 UTC (rev 124) @@ -140,7 +140,6 @@ # extra \r\n and if we clean it now (throw it away), then we # can avoid a regexp later on. if ($self->{ditch_leading_rn} && $self->{headers_string} eq "\r\n") { - print " throwing away leading \\r\\n\n" if $::DEBUG >= 3; $self->{ditch_leading_rn} = 0; $self->{headers_string} = ""; return; @@ -152,12 +151,10 @@ } my $hstr = substr($self->{headers_string}, 0, $idx); - print " pre-parsed headers: [$hstr]\n" if $::DEBUG >= 3; my $extra = substr($self->{headers_string}, $idx+4); if (my $len = length($extra)) { - print " pushing back $len bytes after header\n" if $::DEBUG >= 3; $self->push_back_read(\$extra); } Modified: trunk/lib/AxKit2/Test.pm =================================================================== --- trunk/lib/AxKit2/Test.pm 2006-08-24 18:11:37 UTC (rev 123) +++ trunk/lib/AxKit2/Test.pm 2006-08-24 19:22:32 UTC (rev 124) @@ -19,12 +19,18 @@ use warnings; use IO::Socket; -use base 'Exporter'; +use LWP::UserAgent; +use File::Spec; +use base 'Test::Builder::Module'; -our @EXPORT = qw(start_server); +our @EXPORT = qw(start_server stop_server content_is status_is is_redirect no_redirect); +our $VERSION = 0.01; # Module to assist with testing +my $ua = LWP::UserAgent->new; +$ua->agent(__PACKAGE__."/".$VERSION); + my $server_port = 54000; sub get_free_port { @@ -41,15 +47,99 @@ return $server_port; } +my $server; + +=head2 start_server <config> | <docroot> <plugins> directives + +This takes either a configuration file excerpt as a string (anything that goes inside a <Server></Server> block), +or the document root, a list of plugins to load and a list of other configuration directives. + +=cut + sub start_server { - my $config = shift; + my ($docroot, $plugins, $directives) = @_; my $port = get_free_port(); - return AxKit2::Test::Server->new($port, $config); + if (defined $plugins) { + $directives ||= []; + $docroot = File::Spec->rel2abs($docroot); + $server = AxKit2::Test::Server->new($port,"DocumentRoot $docroot\n" . + join("\n",map { "Plugin $_" } @$plugins) . "\n" . + join("\n",@$directives) . "\n"); + } else { + $server = AxKit2::Test::Server->new($port, $docroot); + } + + return $server; } +sub stop_server { + $server->shutdown(); + undef $server; +} +sub http_get { + my ($url) = @_; + $url = "http://localhost:$server_port$url" if $url !~ m/^[a-z0-9]{1,6}:/i; + my $req = new HTTP::Request(GET => $url); + return ($req, $ua->request($req)); +} + +sub content_is { + my ($url, $content, $name) = @_; + my $builder = __PACKAGE__->builder; + my $res = http_get($url); + if (!$res->is_success) { + $builder->ok(0,$name); + $builder->diag("Request for '${url}' failed with error code ".$res->status_line); + return 0; + } + my $got = $res->content; + $got =~ s/[\r\n]*$//; + $content =~ s/[\r\n]*$//; + $builder->ok($res->content eq $content, $name) or $builder->diag("Request for '${url}' failed: + got: $got +expected: $content"); +} + +sub is_redirect { + my ($url, $dest, $name) = @_; + my $builder = __PACKAGE__->builder; + $ua->max_redirect(0); + $dest = "http://localhost:$server_port$dest"; + my $res = http_get($url); + $ua->max_redirect(7); + my $got = $res->code; + my $gotdest = $res->header('Location'); + $builder->ok($res->is_redirect && $dest eq $gotdest, $name) or $builder->diag("Request for '${url}' failed:" . + ($res->is_redirect? "" : "\n got status: $got, expected a redirect") . + ($dest eq $gotdest? "" : "\n got destination: $gotdest\nexpected destination: $dest")); +} + +sub no_redirect { + my ($url, $dest, $name) = @_; + my $builder = __PACKAGE__->builder; + $ua->max_redirect(0); + $dest = "http://localhost:$server_port$dest"; + my $res = http_get($url); + $ua->max_redirect(7); + my $got = $res->code; + my $gotdest = $res->header('Location'); + $builder->ok(!$res->is_redirect) or $builder->diag("Request for '${url}' failed: + got status: $got -> $gotdest, expected non-redirect status"); +} + +sub status_is { + my ($url, $status, $name) = @_; + my $builder = __PACKAGE__->builder; + my $res = http_get($url); + my $got = $res->code; + $builder->ok($got == $status, $name) or $builder->diag("Request for '${url}' failed: + got status: $got +expected status: $status"); +} + package AxKit2::Test::Server; use File::Temp qw(tempfile); Added: trunk/plugins/typeless_uri =================================================================== --- trunk/plugins/typeless_uri 2006-08-24 18:11:37 UTC (rev 123) +++ trunk/plugins/typeless_uri 2006-08-24 19:22:32 UTC (rev 124) @@ -0,0 +1,102 @@ +#!/usr/bin/perl -w + +# Copyright 2001-2006 The Apache Software Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +=head1 NAME + +typeless_uri - convert typeless URIs (URIs without extension) to working file references + +=head1 SYNOPSIS + + # uri_to_file must go first. + Plugin uri_to_file + Plugin typeless_uri + + # required + DirectoryIndex index + # optionally + URIExtensions html xhtml xsp + +=head1 DESCRIPTION + +This plugin provides the filename for a given URI. It supplements uri_to_file and +provides typeless URIs, i.e. URIs that do not contain a file extension. + +See L<http://www.w3.org/Provider/Style/URI> for a discussion, why this is a Good Thing (TM). + +It works by trying several extensions on the given URI until the resulting file exists. + +=head1 CONFIG + +=head2 URIExtensions STRINGLIST + +A list of file extensions to try in sequence not including the leading dot. + +=head1 TODO + +Content Negotiation should be investigated for another level of flexibility. + +=cut + +use File::Spec::Functions qw(canonpath catfile); +use constant EXTENSIONS => [ + 'xhtml', + 'html', + 'xsp', + 'pl', + 'cgi', +]; + +sub init { + my $self = shift; + $self->register_config('URIExtensions', sub { $self->set_uriextensions(@_) }); +} + +sub set_uriextensions { + my ($self, $config, $value) = @_; + $config->notes($self->plugin_name.'::extensions', [ split(/\s+/,$value) ]); +} + +sub try_extensions { +} + +sub hook_uri_translation { + my ($self, $hd, $uri) = @_; + + my $file = $hd->filename; + return DECLINED if -f $file; + + do { + $file = canonpath(catfile($file,$self->config->notes('uri_to_file::dirindex'))) + if -d _ && !$self->client->notes('need_redirect'); + $self->log(LOGINFO, "typeless: $uri -> $file.*"); + + my $extensions = $self->config('extensions') || EXTENSIONS; + for my $extension (@$extensions) { + if (-f $file.'.'.$extension) { + $hd->filename($file.'.'.$extension); + $self->log(LOGDEBUG, "Translated $uri to ". $hd->filename); + $self->client->notes('need_redirect', 0); + return DECLINED; + } + } + + return DECLINED if ! -d $file || $self->client->notes('need_redirect'); + $file = canonpath(catfile($file,$self->config->notes('uri_to_file::dirindex'))); + } while (1); + + return DECLINED; +} Modified: trunk/plugins/uri_to_file =================================================================== --- trunk/plugins/uri_to_file 2006-08-24 18:11:37 UTC (rev 123) +++ trunk/plugins/uri_to_file 2006-08-24 19:22:32 UTC (rev 124) @@ -44,7 +44,7 @@ =cut -use File::Spec::Functions qw(canonpath catfile); +use File::Spec::Functions qw(canonpath catfile splitdir catdir splitpath catpath); use AxKit2::Utils qw(uri_decode); sub init { @@ -54,8 +54,7 @@ sub set_dirindex { my ($self, $config, $value) = @_; - my $key = $self->plugin_name . '::dirindex'; - $config->notes($key, $value); + $config->notes($self->plugin_name.'::dirindex',$value); } sub hook_uri_translation { @@ -63,9 +62,7 @@ $self->log(LOGINFO, "translate: $uri"); - $uri =~ s/(\?.*)//; - my $removed = $1 || ''; - + $uri =~ s/\?.*//; my $original_uri = $uri; $uri = uri_decode($uri); @@ -78,39 +75,32 @@ $uri =~ s/^\Q$root// || die "$uri did not match config path $root"; - my $path = canonpath(catfile($self->config->docroot, $uri)); - $path .= '/' if $uri =~ /\/$/; # canonpath will strip a trailing slash + my ($volume, $dir, $file) = splitpath($self->config->docroot, 1); + my @path = (splitdir($dir),split(/\//,$uri)); + + my $i = -1; + if (-d catpath($volume,catdir(@path),'')) { + $i = @path-1; + if ($original_uri =~ m/\/$/) { + push @path, $self->config('dirindex') + if (defined $self->config('dirindex') && -f catpath($volume,catdir(@path),$self->config('dirindex'))); + } else { + $self->client->notes('need_redirect',1); + } + } else { + my $path = ''; + foreach my $dir (@path) { + $path = catdir($path,$dir); + last unless -d catpath($volume, $path, ''); + $i++; + } + } + $hd->filename(canonpath(catpath($volume, catdir(@path[0..$i]), ($i+1<@path?$path[$i+1]:'')))); + $hd->path_info(join("/",'',@path[($i+2)..$#path])); + $hd->request_uri(substr($original_uri,0,- length($hd->path_info))) if length($hd->path_info); + $self->log(LOGDEBUG, "Translated $uri to " . $hd->filename . + " (request uri: " . $hd->request_uri . ", path info: " . $hd->path_info . ")"); - my $path_info = ''; - - if (-d $path) { - $self->client->notes('is_dir', 1); - if (my $dirindex = $self->config->notes($self->plugin_name . '::dirindex')) { - my $filepath = catfile($path, $dirindex); - $path = $filepath if -f $filepath; - } - } - else { - while ($path =~ /\// && !-f $path) { - $path =~ s/(\/[^\/]*)$//; - $path_info = $1 . $path_info; - } - if ($path_info && -f _) { - $hd->path_info($path_info); - substr($original_uri, 0 - length($path_info)) = ''; - $hd->request_uri($original_uri); - } - else { - $path .= $path_info; - $hd->path_info(''); - } - } - - $self->log(LOGDEBUG, "Translated $uri to $path" . - ($path_info ? " (path info: $path_info)" : "")); - - $hd->filename($path); - return DECLINED; } @@ -118,7 +108,7 @@ sub hook_fixup { my $self = shift; - return DECLINED unless $self->client->notes('is_dir'); + return DECLINED unless $self->client->notes('need_redirect'); my $uri = $self->client->headers_in->request_uri; @@ -127,9 +117,9 @@ if ($uri =~ s/^([^\?]*)(?<!\/)(\?.*)?$/$1\/$2/) { # send redirect $self->log(LOGINFO, "redirect to $uri"); - $self->client->headers_out->header('Location', "$uri"); + $self->client->headers_out->header('Location', "http://".$self->client->headers_in->header('Host').$uri); return REDIRECT; } - - return DECLINED; + # the above string replace should always succeed + return SERVER_ERROR; } Added: trunk/t/10uri_to_file.t =================================================================== --- trunk/t/10uri_to_file.t 2006-08-24 18:11:37 UTC (rev 123) +++ trunk/t/10uri_to_file.t 2006-08-24 19:22:32 UTC (rev 124) @@ -0,0 +1,15 @@ +#!/usr/bin/perl + +use AxKit2::Test tests => 8; + +start_server("t/server1",[qw(uri_to_file serve_file)],['DirectoryIndex index.html']); + +content_is('/index.html','This is index.html', 'Basic path translation'); +content_is('/','This is index.html', 'DirectoryIndex'); +content_is('/index.html/foobar','This is index.html', 'path_info'); +is_redirect('/foo','/foo/', 'directory redirect'); + +status_is('/index',404, 'nonexistant file'); +status_is('/..',400, 'invalid URL'); +status_is('/i..ndex',400, 'better-safe-than-sorry invalid URL'); +status_is('/i.%2Endex',400, 'hidden invalid URL'); Added: trunk/t/11typeless_uri.t =================================================================== --- trunk/t/11typeless_uri.t 2006-08-24 18:11:37 UTC (rev 123) +++ trunk/t/11typeless_uri.t 2006-08-24 19:22:32 UTC (rev 124) @@ -0,0 +1,20 @@ +#!/usr/bin/perl + +use AxKit2::Test tests => 11; + +start_server("t/server1",[qw(uri_to_file typeless_uri serve_file)],['DirectoryIndex index']); + +content_is('/index.html','This is index.html', 'Basic path translation'); +content_is('/index','This is index.html', 'Basic typeless operation'); +content_is('/','This is index.html', 'typeless DirectoryIndex'); +content_is('/index/foo','This is index.html', 'typeless path_info'); + +is_redirect('/foo','/foo/', 'directory redirect'); +content_is('/foo','This is foo/index.html', 'directory redirect plus DirectoryIndex'); + +no_redirect('/multi', 'no typeless directory redirect'); +content_is('/multi','This is multi.html', 'typeless plus directory'); +content_is('/multi/','This is multi/index.html', 'typeless plus DirectoryIndex'); + +status_is('/index.foo',404, 'nonexistant file'); +status_is('/bar',404, 'nonexistant file'); Added: trunk/t/server1/foo/index.html =================================================================== --- trunk/t/server1/foo/index.html 2006-08-24 18:11:37 UTC (rev 123) +++ trunk/t/server1/foo/index.html 2006-08-24 19:22:32 UTC (rev 124) @@ -0,0 +1 @@ +This is foo/index.html \ No newline at end of file Added: trunk/t/server1/index.html =================================================================== --- trunk/t/server1/index.html 2006-08-24 18:11:37 UTC (rev 123) +++ trunk/t/server1/index.html 2006-08-24 19:22:32 UTC (rev 124) @@ -0,0 +1 @@ +This is index.html \ No newline at end of file Added: trunk/t/server1/multi/index.html =================================================================== --- trunk/t/server1/multi/index.html 2006-08-24 18:11:37 UTC (rev 123) +++ trunk/t/server1/multi/index.html 2006-08-24 19:22:32 UTC (rev 124) @@ -0,0 +1 @@ +This is multi/index.html \ No newline at end of file Added: trunk/t/server1/multi.html =================================================================== --- trunk/t/server1/multi.html 2006-08-24 18:11:37 UTC (rev 123) +++ trunk/t/server1/multi.html 2006-08-24 19:22:32 UTC (rev 124) @@ -0,0 +1 @@ +This is multi.html \ No newline at end of file