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