Add HTTP::Tiny as a dual-life core module
authorDavid Golden <dagolden@cpan.org>
Mon, 17 Jan 2011 01:57:02 +0000 (20:57 -0500)
committerDavid Golden <dagolden@cpan.org>
Mon, 17 Jan 2011 02:05:32 +0000 (21:05 -0500)
HTTP::Tiny 0.008 has been added as a dual-life module.  It is a very
small, simple HTTP/1.1 client designed for simple GET requests and file
mirroring.  It has has been added to enable CPAN.pm and CPANPLUS to
"bootstrap" HTTP access to CPAN using pure Perl without relying on external
binaries like F<curl> or F<wget>.

60 files changed:
MANIFEST
Porting/Maintainers.pl
cpan/HTTP-Tiny/lib/HTTP/Tiny.pm [new file with mode: 0644]
cpan/HTTP-Tiny/t/00-compile.t [new file with mode: 0644]
cpan/HTTP-Tiny/t/000_load.t [new file with mode: 0644]
cpan/HTTP-Tiny/t/001_api.t [new file with mode: 0644]
cpan/HTTP-Tiny/t/002_croakage.t [new file with mode: 0644]
cpan/HTTP-Tiny/t/010_url.t [new file with mode: 0644]
cpan/HTTP-Tiny/t/020_headers.t [new file with mode: 0644]
cpan/HTTP-Tiny/t/030_response.t [new file with mode: 0644]
cpan/HTTP-Tiny/t/040_content.t [new file with mode: 0644]
cpan/HTTP-Tiny/t/050_chunked_body.t [new file with mode: 0644]
cpan/HTTP-Tiny/t/060_http_date.t [new file with mode: 0644]
cpan/HTTP-Tiny/t/100_get.t [new file with mode: 0644]
cpan/HTTP-Tiny/t/110_mirror.t [new file with mode: 0644]
cpan/HTTP-Tiny/t/120_put.t [new file with mode: 0644]
cpan/HTTP-Tiny/t/130_redirect.t [new file with mode: 0644]
cpan/HTTP-Tiny/t/Util.pm [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/get-01.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/get-02.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/get-03.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/get-04.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/get-05.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/get-06.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/get-07.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/get-08.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/get-09.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/get-10.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/get-11.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/get-12.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/get-13.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/get-14.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/get-15.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/get-16.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/get-17.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/get-18.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/get-19.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/get-20.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/get-21.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/mirror-01.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/mirror-02.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/mirror-03.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/mirror-04.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/mirror-05.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/put-01.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/put-02.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/put-03.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/put-04.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/put-05.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/redirect-01.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/redirect-02.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/redirect-03.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/redirect-04.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/redirect-05.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/redirect-06.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/redirect-07.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/redirect-08.txt [new file with mode: 0644]
cpan/HTTP-Tiny/t/cases/redirect-09.txt [new file with mode: 0644]
lib/.gitignore
pod/perldelta.pod

index a031bc0..20d71f6 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -1029,6 +1029,62 @@ cpan/Getopt-Long/t/gol-linkage.t See if Getopt::Long works
 cpan/Getopt-Long/t/gol-oo.t            See if Getopt::Long works
 cpan/Getopt-Long/t/gol-xargv.t         See if Getopt::Long works
 cpan/Getopt-Long/t/gol-xstring.t       See if Getopt::Long works
+cpan/HTTP-Tiny/lib/HTTP/Tiny.pm
+cpan/HTTP-Tiny/t/000_load.t
+cpan/HTTP-Tiny/t/001_api.t
+cpan/HTTP-Tiny/t/002_croakage.t
+cpan/HTTP-Tiny/t/00-compile.t
+cpan/HTTP-Tiny/t/010_url.t
+cpan/HTTP-Tiny/t/020_headers.t
+cpan/HTTP-Tiny/t/030_response.t
+cpan/HTTP-Tiny/t/040_content.t
+cpan/HTTP-Tiny/t/050_chunked_body.t
+cpan/HTTP-Tiny/t/060_http_date.t
+cpan/HTTP-Tiny/t/100_get.t
+cpan/HTTP-Tiny/t/110_mirror.t
+cpan/HTTP-Tiny/t/120_put.t
+cpan/HTTP-Tiny/t/130_redirect.t
+cpan/HTTP-Tiny/t/cases/get-01.txt
+cpan/HTTP-Tiny/t/cases/get-02.txt
+cpan/HTTP-Tiny/t/cases/get-03.txt
+cpan/HTTP-Tiny/t/cases/get-04.txt
+cpan/HTTP-Tiny/t/cases/get-05.txt
+cpan/HTTP-Tiny/t/cases/get-06.txt
+cpan/HTTP-Tiny/t/cases/get-07.txt
+cpan/HTTP-Tiny/t/cases/get-08.txt
+cpan/HTTP-Tiny/t/cases/get-09.txt
+cpan/HTTP-Tiny/t/cases/get-10.txt
+cpan/HTTP-Tiny/t/cases/get-11.txt
+cpan/HTTP-Tiny/t/cases/get-12.txt
+cpan/HTTP-Tiny/t/cases/get-13.txt
+cpan/HTTP-Tiny/t/cases/get-14.txt
+cpan/HTTP-Tiny/t/cases/get-15.txt
+cpan/HTTP-Tiny/t/cases/get-16.txt
+cpan/HTTP-Tiny/t/cases/get-17.txt
+cpan/HTTP-Tiny/t/cases/get-18.txt
+cpan/HTTP-Tiny/t/cases/get-19.txt
+cpan/HTTP-Tiny/t/cases/get-20.txt
+cpan/HTTP-Tiny/t/cases/get-21.txt
+cpan/HTTP-Tiny/t/cases/mirror-01.txt
+cpan/HTTP-Tiny/t/cases/mirror-02.txt
+cpan/HTTP-Tiny/t/cases/mirror-03.txt
+cpan/HTTP-Tiny/t/cases/mirror-04.txt
+cpan/HTTP-Tiny/t/cases/mirror-05.txt
+cpan/HTTP-Tiny/t/cases/put-01.txt
+cpan/HTTP-Tiny/t/cases/put-02.txt
+cpan/HTTP-Tiny/t/cases/put-03.txt
+cpan/HTTP-Tiny/t/cases/put-04.txt
+cpan/HTTP-Tiny/t/cases/put-05.txt
+cpan/HTTP-Tiny/t/cases/redirect-01.txt
+cpan/HTTP-Tiny/t/cases/redirect-02.txt
+cpan/HTTP-Tiny/t/cases/redirect-03.txt
+cpan/HTTP-Tiny/t/cases/redirect-04.txt
+cpan/HTTP-Tiny/t/cases/redirect-05.txt
+cpan/HTTP-Tiny/t/cases/redirect-06.txt
+cpan/HTTP-Tiny/t/cases/redirect-07.txt
+cpan/HTTP-Tiny/t/cases/redirect-08.txt
+cpan/HTTP-Tiny/t/cases/redirect-09.txt
+cpan/HTTP-Tiny/t/Util.pm
 cpan/IO-Compress/Changes                               IO::Compress
 cpan/IO-Compress/examples/compress-zlib/filtdef                IO::Compress
 cpan/IO-Compress/examples/compress-zlib/filtinf                IO::Compress
index 6057eda..a212efc 100755 (executable)
@@ -765,6 +765,19 @@ use File::Glob qw(:case);
        'UPSTREAM'      => 'cpan',
        },
 
+    'HTTP::Tiny' =>
+       {
+       'MAINTAINER'    => 'dagolden',
+       'DISTRIBUTION'  => 'DAGOLDEN/HTTP-Tiny-0.008.tar.gz',
+       'FILES'         => q[cpan/HTTP-Tiny],
+       'EXCLUDED'      => [
+                               't/200_live.t',
+                               qr/^eg/,
+                               qr/^xt/
+                          ],
+       'UPSTREAM'      => 'cpan',
+       },
+
     'I18N::Collate' =>
        {
        'MAINTAINER'    => 'p5p',
diff --git a/cpan/HTTP-Tiny/lib/HTTP/Tiny.pm b/cpan/HTTP-Tiny/lib/HTTP/Tiny.pm
new file mode 100644 (file)
index 0000000..5a22982
--- /dev/null
@@ -0,0 +1,1069 @@
+# vim: ts=4 sts=4 sw=4 et:
+#
+# This file is part of HTTP-Tiny
+#
+# This software is copyright (c) 2011 by Christian Hansen.
+#
+# This is free software; you can redistribute it and/or modify it under
+# the same terms as the Perl 5 programming language system itself.
+#
+package HTTP::Tiny;
+BEGIN {
+  $HTTP::Tiny::VERSION = '0.008';
+}
+use strict;
+use warnings;
+# ABSTRACT: A small, simple, correct HTTP/1.1 client
+
+use Carp ();
+
+
+my @attributes;
+BEGIN {
+    @attributes = qw(agent default_headers max_redirect max_size proxy timeout);
+    no strict 'refs';
+    for my $accessor ( @attributes ) {
+        *{$accessor} = sub {
+            @_ > 1 ? $_[0]->{$accessor} = $_[1] : $_[0]->{$accessor};
+        };
+    }
+}
+
+sub new {
+    my($class, %args) = @_;
+    (my $agent = $class) =~ s{::}{-}g;
+    my $self = {
+        agent        => $agent . "/" . ($class->VERSION || 0),
+        max_redirect => 5,
+        timeout      => 60,
+    };
+    for my $key ( @attributes ) {
+        $self->{$key} = $args{$key} if exists $args{$key}
+    }
+    return bless $self, $class;
+}
+
+
+sub get {
+    my ($self, $url, $args) = @_;
+    @_ == 2 || (@_ == 3 && ref $args eq 'HASH')
+      or Carp::croak(q/Usage: $http->get(URL, [HASHREF])/);
+    return $self->request('GET', $url, $args || {});
+}
+
+
+sub mirror {
+    my ($self, $url, $file, $args) = @_;
+    @_ == 3 || (@_ == 4 && ref $args eq 'HASH')
+      or Carp::croak(q/Usage: $http->mirror(URL, FILE, [HASHREF])/);
+    if ( -e $file and my $mtime = (stat($file))[9] ) {
+        $args->{headers}{'if-modified-since'} ||= $self->_http_date($mtime);
+    }
+    my $tempfile = $file . int(rand(2**31));
+    open my $fh, ">", $tempfile
+        or Carp::croak(qq/Error: Could not open temporary file $tempfile for downloading: $!/);
+    $args->{data_callback} = sub { print {$fh} $_[0] };
+    my $response = $self->request('GET', $url, $args);
+    close $fh
+        or Carp::croak(qq/Error: Could not close temporary file $tempfile: $!/);
+    if ( $response->{success} ) {
+        rename $tempfile, $file
+            or Carp::croak "Error replacing $file with $tempfile: $!\n";
+        my $lm = $response->{headers}{'last-modified'};
+        if ( $lm and my $mtime = $self->_parse_http_date($lm) ) {
+            utime $mtime, $mtime, $file;
+        }
+    }
+    $response->{success} ||= $response->{status} eq '304';
+    unlink $tempfile;
+    return $response;
+}
+
+
+my %idempotent = map { $_ => 1 } qw/GET HEAD PUT DELETE OPTIONS TRACE/;
+
+sub request {
+    my ($self, $method, $url, $args) = @_;
+    @_ == 3 || (@_ == 4 && ref $args eq 'HASH')
+      or Carp::croak(q/Usage: $http->request(METHOD, URL, [HASHREF])/);
+    $args ||= {}; # we keep some state in this during _request
+
+    # RFC 2616 Section 8.1.4 mandates a single retry on broken socket
+    my $response;
+    for ( 0 .. 1 ) {
+        $response = eval { $self->_request($method, $url, $args) };
+        last unless $@ && $idempotent{$method}
+            && $@ =~ m{^(?:Socket closed|Unexpected end)};
+    }
+
+    if (my $e = "$@") {
+        $response = {
+            success => q{},
+            status  => 599,
+            reason  => 'Internal Exception',
+            content => $e,
+            headers => {
+                'content-type'   => 'text/plain',
+                'content-length' => length $e,
+            }
+        };
+    }
+    return $response;
+}
+
+my %DefaultPort = (
+    http => 80,
+    https => 443,
+);
+
+sub _request {
+    my ($self, $method, $url, $args) = @_;
+
+    my ($scheme, $host, $port, $path_query) = $self->_split_url($url);
+
+    my $request = {
+        method    => $method,
+        scheme    => $scheme,
+        host_port => ($port == $DefaultPort{$scheme} ? $host : "$host:$port"),
+        uri       => $path_query,
+        headers   => {},
+    };
+
+    my $handle  = HTTP::Tiny::Handle->new(timeout => $self->{timeout});
+
+    if ($self->{proxy}) {
+        $request->{uri} = "$scheme://$request->{host_port}$path_query";
+        croak(qq/HTTPS via proxy is not supported/)
+            if $request->{scheme} eq 'https';
+        $handle->connect(($self->_split_url($self->{proxy}))[0..2]);
+    }
+    else {
+        $handle->connect($scheme, $host, $port);
+    }
+
+    $self->_prepare_headers_and_cb($request, $args);
+    $handle->write_request($request);
+
+    my $response;
+    do { $response = $handle->read_response_header }
+        until (substr($response->{status},0,1) ne '1');
+
+    if ( my @redir_args = $self->_maybe_redirect($request, $response, $args) ) {
+        $handle->close;
+        return $self->_request(@redir_args, $args);
+    }
+
+    if ($method eq 'HEAD' || $response->{status} =~ /^[23]04/) {
+        # response has no message body
+    }
+    else {
+        my $data_cb = $self->_prepare_data_cb($response, $args);
+        $handle->read_body($data_cb, $response);
+    }
+
+    $handle->close;
+    $response->{success} = substr($response->{status},0,1) eq '2';
+    return $response;
+}
+
+sub _prepare_headers_and_cb {
+    my ($self, $request, $args) = @_;
+
+    for ($self->{default_headers}, $args->{headers}) {
+        next unless defined;
+        while (my ($k, $v) = each %$_) {
+            $request->{headers}{lc $k} = $v;
+        }
+    }
+    $request->{headers}{'host'}         = $request->{host_port};
+    $request->{headers}{'connection'}   = "close";
+    $request->{headers}{'user-agent'} ||= $self->{agent};
+
+    if (defined $args->{content}) {
+        $request->{headers}{'content-type'} ||= "application/octet-stream";
+        if (ref $args->{content} eq 'CODE') {
+            $request->{headers}{'transfer-encoding'} = 'chunked'
+              unless $request->{headers}{'content-length'}
+                  || $request->{headers}{'transfer-encoding'};
+            $request->{cb} = $args->{content};
+        }
+        else {
+            my $content = $args->{content};
+            if ( $] ge '5.008' ) {
+                utf8::downgrade($content, 1)
+                    or Carp::croak(q/Wide character in request message body/);
+            }
+            $request->{headers}{'content-length'} = length $content
+              unless $request->{headers}{'content-length'}
+                  || $request->{headers}{'transfer-encoding'};
+            $request->{cb} = sub { substr $content, 0, length $content, '' };
+        }
+        $request->{trailer_cb} = $args->{trailer_callback}
+            if ref $args->{trailer_callback} eq 'CODE';
+    }
+    return;
+}
+
+sub _prepare_data_cb {
+    my ($self, $response, $args) = @_;
+    my $data_cb = $args->{data_callback};
+    $response->{content} = '';
+
+    if (!$data_cb || $response->{status} !~ /^2/) {
+        if (defined $self->{max_size}) {
+            $data_cb = sub {
+                $_[1]->{content} .= $_[0];
+                die(qq/Size of response body exceeds the maximum allowed of $self->{max_size}\n/)
+                  if length $_[1]->{content} > $self->{max_size};
+            };
+        }
+        else {
+            $data_cb = sub { $_[1]->{content} .= $_[0] };
+        }
+    }
+    return $data_cb;
+}
+
+sub _maybe_redirect {
+    my ($self, $request, $response, $args) = @_;
+    my $headers = $response->{headers};
+    my ($status, $method) = ($response->{status}, $request->{method});
+    if (($status eq '303' or ($status =~ /^30[127]/ && $method =~ /^GET|HEAD$/))
+        and $headers->{location}
+        and ++$args->{redirects} <= $self->{max_redirect}
+    ) {
+        my $location = ($headers->{location} =~ /^\//)
+            ? "$request->{scheme}://$request->{host_port}$headers->{location}"
+            : $headers->{location} ;
+        return (($status eq '303' ? 'GET' : $method), $location);
+    }
+    return;
+}
+
+sub _split_url {
+    my $url = pop;
+
+    # URI regex adapted from the URI module
+    my ($scheme, $authority, $path_query) = $url =~ m<\A([^:/?#]+)://([^/?#]*)([^#]*)>
+      or Carp::croak(qq/Cannot parse URL: '$url'/);
+
+    $scheme     = lc $scheme;
+    $path_query = "/$path_query" unless $path_query =~ m<\A/>;
+
+    my $host = (length($authority)) ? lc $authority : 'localhost';
+       $host =~ s/\A[^@]*@//;   # userinfo
+    my $port = do {
+       $host =~ s/:([0-9]*)\z// && length $1
+         ? $1
+         : ($scheme eq 'http' ? 80 : $scheme eq 'https' ? 443 : undef);
+    };
+
+    return ($scheme, $host, $port, $path_query);
+}
+
+# Date conversions adapted from HTTP::Date
+my $DoW = "Sun|Mon|Tue|Wed|Thu|Fri|Sat";
+my $MoY = "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec";
+sub _http_date {
+    my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime($_[1]);
+    return sprintf("%s, %02d %s %04d %02d:%02d:%02d GMT",
+        substr($DoW,$wday*4,3),
+        $mday, substr($MoY,$mon*4,3), $year+1900,
+        $hour, $min, $sec
+    );
+}
+
+sub _parse_http_date {
+    my ($self, $str) = @_;
+    require Time::Local;
+    my @tl_parts;
+    if ($str =~ /^[SMTWF][a-z]+, +(\d{1,2}) ($MoY) +(\d\d\d\d) +(\d\d):(\d\d):(\d\d) +GMT$/) {
+        @tl_parts = ($6, $5, $4, $1, (index($MoY,$2)/4), $3);
+    }
+    elsif ($str =~ /^[SMTWF][a-z]+, +(\d\d)-($MoY)-(\d{2,4}) +(\d\d):(\d\d):(\d\d) +GMT$/ ) {
+        @tl_parts = ($6, $5, $4, $1, (index($MoY,$2)/4), $3);
+    }
+    elsif ($str =~ /^[SMTWF][a-z]+ +($MoY) +(\d{1,2}) +(\d\d):(\d\d):(\d\d) +(?:[^0-9]+ +)?(\d\d\d\d)$/ ) {
+        @tl_parts = ($5, $4, $3, $2, (index($MoY,$1)/4), $6);
+    }
+    return eval {
+        my $t = @tl_parts ? Time::Local::timegm(@tl_parts) : -1;
+        $t < 0 ? undef : $t;
+    };
+}
+
+package
+    HTTP::Tiny::Handle; # hide from PAUSE/indexers
+use strict;
+use warnings;
+
+use Carp       qw[croak];
+use Errno      qw[EINTR EPIPE];
+use IO::Socket qw[SOCK_STREAM];
+
+sub BUFSIZE () { 32768 }
+
+my $Printable = sub {
+    local $_ = shift;
+    s/\r/\\r/g;
+    s/\n/\\n/g;
+    s/\t/\\t/g;
+    s/([^\x20-\x7E])/sprintf('\\x%.2X', ord($1))/ge;
+    $_;
+};
+
+my $Token = qr/[\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7A\x7C\x7E]/;
+
+sub new {
+    my ($class, %args) = @_;
+    return bless {
+        rbuf             => '',
+        timeout          => 60,
+        max_line_size    => 16384,
+        max_header_lines => 64,
+        %args
+    }, $class;
+}
+
+sub connect {
+    @_ == 4 || croak(q/Usage: $handle->connect(scheme, host, port)/);
+    my ($self, $scheme, $host, $port) = @_;
+
+    if ( $scheme eq 'https' ) {
+        eval "require IO::Socket::SSL"
+            unless exists $INC{'IO/Socket/SSL.pm'};
+        croak(qq/IO::Socket::SSL must be installed for https support\n/)
+            unless $INC{'IO/Socket/SSL.pm'};
+    }
+    elsif ( $scheme ne 'http' ) {
+      croak(qq/Unsupported URL scheme '$scheme'/);
+    }
+
+    $self->{fh} = 'IO::Socket::INET'->new(
+        PeerHost  => $host,
+        PeerPort  => $port,
+        Proto     => 'tcp',
+        Type      => SOCK_STREAM,
+        Timeout   => $self->{timeout}
+    ) or croak(qq/Could not connect to '$host:$port': $@/);
+
+    binmode($self->{fh})
+      or croak(qq/Could not binmode() socket: '$!'/);
+
+    if ( $scheme eq 'https') {
+        IO::Socket::SSL->start_SSL($self->{fh});
+        ref($self->{fh}) eq 'IO::Socket::SSL'
+            and $self->{fh}->verify_hostname( $host, 'http' )
+            or croak(qq/SSL connection failed for $host\n/);
+    }
+
+    $self->{host} = $host;
+    $self->{port} = $port;
+
+    return $self;
+}
+
+sub close {
+    @_ == 1 || croak(q/Usage: $handle->close()/);
+    my ($self) = @_;
+    CORE::close($self->{fh})
+      or croak(qq/Could not close socket: '$!'/);
+}
+
+sub write {
+    @_ == 2 || croak(q/Usage: $handle->write(buf)/);
+    my ($self, $buf) = @_;
+
+    if ( $] ge '5.008' ) {
+        utf8::downgrade($buf, 1)
+            or croak(q/Wide character in write()/);
+    }
+
+    my $len = length $buf;
+    my $off = 0;
+
+    local $SIG{PIPE} = 'IGNORE';
+
+    while () {
+        $self->can_write
+          or croak(q/Timed out while waiting for socket to become ready for writing/);
+        my $r = syswrite($self->{fh}, $buf, $len, $off);
+        if (defined $r) {
+            $len -= $r;
+            $off += $r;
+            last unless $len > 0;
+        }
+        elsif ($! == EPIPE) {
+            croak(qq/Socket closed by remote server: $!/);
+        }
+        elsif ($! != EINTR) {
+            croak(qq/Could not write to socket: '$!'/);
+        }
+    }
+    return $off;
+}
+
+sub read {
+    @_ == 2 || @_ == 3 || croak(q/Usage: $handle->read(len [, allow_partial])/);
+    my ($self, $len, $allow_partial) = @_;
+
+    my $buf  = '';
+    my $got = length $self->{rbuf};
+
+    if ($got) {
+        my $take = ($got < $len) ? $got : $len;
+        $buf  = substr($self->{rbuf}, 0, $take, '');
+        $len -= $take;
+    }
+
+    while ($len > 0) {
+        $self->can_read
+          or croak(q/Timed out while waiting for socket to become ready for reading/);
+        my $r = sysread($self->{fh}, $buf, $len, length $buf);
+        if (defined $r) {
+            last unless $r;
+            $len -= $r;
+        }
+        elsif ($! != EINTR) {
+            croak(qq/Could not read from socket: '$!'/);
+        }
+    }
+    if ($len && !$allow_partial) {
+        croak(q/Unexpected end of stream/);
+    }
+    return $buf;
+}
+
+sub readline {
+    @_ == 1 || croak(q/Usage: $handle->readline()/);
+    my ($self) = @_;
+
+    while () {
+        if ($self->{rbuf} =~ s/\A ([^\x0D\x0A]* \x0D?\x0A)//x) {
+            return $1;
+        }
+        if (length $self->{rbuf} >= $self->{max_line_size}) {
+            croak(qq/Line size exceeds the maximum allowed size of $self->{max_line_size}/);
+        }
+        $self->can_read
+          or croak(q/Timed out while waiting for socket to become ready for reading/);
+        my $r = sysread($self->{fh}, $self->{rbuf}, BUFSIZE, length $self->{rbuf});
+        if (defined $r) {
+            last unless $r;
+        }
+        elsif ($! != EINTR) {
+            croak(qq/Could not read from socket: '$!'/);
+        }
+    }
+    croak(q/Unexpected end of stream while looking for line/);
+}
+
+sub read_header_lines {
+    @_ == 1 || @_ == 2 || croak(q/Usage: $handle->read_header_lines([headers])/);
+    my ($self, $headers) = @_;
+    $headers ||= {};
+    my $lines   = 0;
+    my $val;
+
+    while () {
+         my $line = $self->readline;
+
+         if (++$lines >= $self->{max_header_lines}) {
+             croak(qq/Header lines exceeds maximum number allowed of $self->{max_header_lines}/);
+         }
+         elsif ($line =~ /\A ([^\x00-\x1F\x7F:]+) : [\x09\x20]* ([^\x0D\x0A]*)/x) {
+             my ($field_name) = lc $1;
+             if (exists $headers->{$field_name}) {
+                 for ($headers->{$field_name}) {
+                     $_ = [$_] unless ref $_ eq "ARRAY";
+                     push @$_, $2;
+                     $val = \$_->[-1];
+                 }
+             }
+             else {
+                 $val = \($headers->{$field_name} = $2);
+             }
+         }
+         elsif ($line =~ /\A [\x09\x20]+ ([^\x0D\x0A]*)/x) {
+             $val
+               or croak(q/Unexpected header continuation line/);
+             next unless length $1;
+             $$val .= ' ' if length $$val;
+             $$val .= $1;
+         }
+         elsif ($line =~ /\A \x0D?\x0A \z/x) {
+            last;
+         }
+         else {
+            croak(q/Malformed header line: / . $Printable->($line));
+         }
+    }
+    return $headers;
+}
+
+sub write_request {
+    @_ == 2 || croak(q/Usage: $handle->write_request(request)/);
+    my($self, $request) = @_;
+    $self->write_request_header(@{$request}{qw/method uri headers/});
+    $self->write_body($request) if $request->{cb};
+    return;
+}
+
+my %HeaderCase = (
+    'content-md5'      => 'Content-MD5',
+    'etag'             => 'ETag',
+    'te'               => 'TE',
+    'www-authenticate' => 'WWW-Authenticate',
+    'x-xss-protection' => 'X-XSS-Protection',
+);
+
+sub write_header_lines {
+    (@_ == 2 && ref $_[1] eq 'HASH') || croak(q/Usage: $handle->write_header_lines(headers)/);
+    my($self, $headers) = @_;
+
+    my $buf = '';
+    while (my ($k, $v) = each %$headers) {
+        my $field_name = lc $k;
+        if (exists $HeaderCase{$field_name}) {
+            $field_name = $HeaderCase{$field_name};
+        }
+        else {
+            $field_name =~ /\A $Token+ \z/xo
+              or croak(q/Invalid HTTP header field name: / . $Printable->($field_name));
+            $field_name =~ s/\b(\w)/\u$1/g;
+            $HeaderCase{lc $field_name} = $field_name;
+        }
+        for (ref $v eq 'ARRAY' ? @$v : $v) {
+            /[^\x0D\x0A]/
+              or croak(qq/Invalid HTTP header field value ($field_name): / . $Printable->($_));
+            $buf .= "$field_name: $_\x0D\x0A";
+        }
+    }
+    $buf .= "\x0D\x0A";
+    return $self->write($buf);
+}
+
+sub read_body {
+    @_ == 3 || croak(q/Usage: $handle->read_body(callback, response)/);
+    my ($self, $cb, $response) = @_;
+    my $te = $response->{headers}{'transfer-encoding'} || '';
+    if ( grep { /chunked/i } ( ref $te eq 'ARRAY' ? @$te : $te ) ) {
+        $self->read_chunked_body($cb, $response);
+    }
+    else {
+        $self->read_content_body($cb, $response);
+    }
+    return;
+}
+
+sub write_body {
+    @_ == 2 || croak(q/Usage: $handle->write_body(request)/);
+    my ($self, $request) = @_;
+    if ($request->{headers}{'content-length'}) {
+        return $self->write_content_body($request);
+    }
+    else {
+        return $self->write_chunked_body($request);
+    }
+}
+
+sub read_content_body {
+    @_ == 3 || @_ == 4 || croak(q/Usage: $handle->read_content_body(callback, response, [read_length])/);
+    my ($self, $cb, $response, $content_length) = @_;
+    $content_length ||= $response->{headers}{'content-length'};
+
+    if ( $content_length ) {
+        my $len = $content_length;
+        while ($len > 0) {
+            my $read = ($len > BUFSIZE) ? BUFSIZE : $len;
+            $cb->($self->read($read, 0), $response);
+            $len -= $read;
+        }
+    }
+    else {
+        my $chunk;
+        $cb->($chunk, $response) while length( $chunk = $self->read(BUFSIZE, 1) );
+    }
+
+    return;
+}
+
+sub write_content_body {
+    @_ == 2 || croak(q/Usage: $handle->write_content_body(request)/);
+    my ($self, $request) = @_;
+
+    my ($len, $content_length) = (0, $request->{headers}{'content-length'});
+    while () {
+        my $data = $request->{cb}->();
+
+        defined $data && length $data
+          or last;
+
+        if ( $] ge '5.008' ) {
+            utf8::downgrade($data, 1)
+                or croak(q/Wide character in write_content()/);
+        }
+
+        $len += $self->write($data);
+    }
+
+    $len == $content_length
+      or croak(qq/Content-Length missmatch (got: $len expected: $content_length)/);
+
+    return $len;
+}
+
+sub read_chunked_body {
+    @_ == 3 || croak(q/Usage: $handle->read_chunked_body(callback, $response)/);
+    my ($self, $cb, $response) = @_;
+
+    while () {
+        my $head = $self->readline;
+
+        $head =~ /\A ([A-Fa-f0-9]+)/x
+          or croak(q/Malformed chunk head: / . $Printable->($head));
+
+        my $len = hex($1)
+          or last;
+
+        $self->read_content_body($cb, $response, $len);
+
+        $self->read(2) eq "\x0D\x0A"
+          or croak(q/Malformed chunk: missing CRLF after chunk data/);
+    }
+    $self->read_header_lines($response->{headers});
+    return;
+}
+
+sub write_chunked_body {
+    @_ == 2 || croak(q/Usage: $handle->write_chunked_body(request)/);
+    my ($self, $request) = @_;
+
+    my $len = 0;
+    while () {
+        my $data = $request->{cb}->();
+
+        defined $data && length $data
+          or last;
+
+        if ( $] ge '5.008' ) {
+            utf8::downgrade($data, 1)
+                or croak(q/Wide character in write_chunked_body()/);
+        }
+
+        $len += length $data;
+
+        my $chunk  = sprintf '%X', length $data;
+           $chunk .= "\x0D\x0A";
+           $chunk .= $data;
+           $chunk .= "\x0D\x0A";
+
+        $self->write($chunk);
+    }
+    $self->write("0\x0D\x0A");
+    $self->write_header_lines($request->{trailer_cb}->())
+        if ref $request->{trailer_cb} eq 'CODE';
+    return $len;
+}
+
+sub read_response_header {
+    @_ == 1 || croak(q/Usage: $handle->read_response_header()/);
+    my ($self) = @_;
+
+    my $line = $self->readline;
+
+    $line =~ /\A (HTTP\/(0*\d+\.0*\d+)) [\x09\x20]+ ([0-9]{3}) [\x09\x20]+ ([^\x0D\x0A]*) \x0D?\x0A/x
+      or croak(q/Malformed Status-Line: / . $Printable->($line));
+
+    my ($protocol, $version, $status, $reason) = ($1, $2, $3, $4);
+
+    croak (qq/Unsupported HTTP protocol: $protocol/)
+        unless $version =~ /0*1\.0*[01]/;
+
+    return {
+        status   => $status,
+        reason   => $reason,
+        headers  => $self->read_header_lines,
+        protocol => $protocol,
+    };
+}
+
+sub write_request_header {
+    @_ == 4 || croak(q/Usage: $handle->write_request_header(method, request_uri, headers)/);
+    my ($self, $method, $request_uri, $headers) = @_;
+
+    return $self->write("$method $request_uri HTTP/1.1\x0D\x0A")
+         + $self->write_header_lines($headers);
+}
+
+sub _do_timeout {
+    my ($self, $type, $timeout) = @_;
+    $timeout = $self->{timeout}
+        unless defined $timeout && $timeout >= 0;
+
+    my $fd = fileno $self->{fh};
+    defined $fd && $fd >= 0
+      or croak(q/select(2): 'Bad file descriptor'/);
+
+    my $initial = time;
+    my $pending = $timeout;
+    my $nfound;
+
+    vec(my $fdset = '', $fd, 1) = 1;
+
+    while () {
+        $nfound = ($type eq 'read')
+            ? select($fdset, undef, undef, $pending)
+            : select(undef, $fdset, undef, $pending) ;
+        if ($nfound == -1) {
+            $! == EINTR
+              or croak(qq/select(2): '$!'/);
+            redo if !$timeout || ($pending = $timeout - (time - $initial)) > 0;
+            $nfound = 0;
+        }
+        last;
+    }
+    $! = 0;
+    return $nfound;
+}
+
+sub can_read {
+    @_ == 1 || @_ == 2 || croak(q/Usage: $handle->can_read([timeout])/);
+    my $self = shift;
+    return $self->_do_timeout('read', @_)
+}
+
+sub can_write {
+    @_ == 1 || @_ == 2 || croak(q/Usage: $handle->can_write([timeout])/);
+    my $self = shift;
+    return $self->_do_timeout('write', @_)
+}
+
+1;
+
+
+
+__END__
+=pod
+
+=head1 NAME
+
+HTTP::Tiny - A small, simple, correct HTTP/1.1 client
+
+=head1 VERSION
+
+version 0.008
+
+=head1 SYNOPSIS
+
+    use HTTP::Tiny;
+
+    my $response = HTTP::Tiny->new->get('http://example.com/');
+
+    die "Failed!\n" unless $response->{success};
+
+    print "$response->{status} $response->{reason}\n";
+
+    while (my ($k, $v) = each %{$response->{headers}}) {
+        for (ref $v eq 'ARRAY' ? @$v : $v) {
+            print "$k: $_\n";
+        }
+    }
+
+    print $response->{content} if length $response->{content};
+
+=head1 DESCRIPTION
+
+This is a very simple HTTP/1.1 client, designed primarily for doing simple GET
+requests without the overhead of a large framework like L<LWP::UserAgent>.
+
+It is more correct and more complete than L<HTTP::Lite>.  It supports
+proxies (currently only non-authenticating ones) and redirection.  It
+also correctly resumes after EINTR.
+
+=head1 METHODS
+
+=head2 new
+
+    $http = HTTP::Tiny->new( %attributes );
+
+This constructor returns a new HTTP::Tiny object.  Valid attributes include:
+
+=over 4
+
+=item *
+
+agent
+
+A user-agent string (defaults to 'HTTP::Tiny/$VERSION')
+
+=item *
+
+default_headers
+
+A hashref of default headers to apply to requests
+
+=item *
+
+max_redirect
+
+Maximum number of redirects allowed (defaults to 5)
+
+=item *
+
+max_size
+
+Maximum response size (only when not using a data callback).  If defined,
+responses larger than this will die with an error message
+
+=item *
+
+proxy
+
+URL of a proxy server to use.
+
+=item *
+
+timeout
+
+Request timeout in seconds (default is 60)
+
+=back
+
+=head2 get
+
+    $response = $http->get($url);
+    $response = $http->get($url, \%options);
+
+Executes a C<GET> request for the given URL.  The URL must have unsafe
+characters escaped and international domain names encoded.  Internally, it just
+calls C<request()> with 'GET' as the method.  See C<request()> for valid
+options and a description of the response.
+
+=head2 mirror
+
+    $response = $http->mirror($url, $file, \%options)
+    if ( $response->{success} ) {
+        print "$file is up to date\n";
+    }
+
+Executes a C<GET> request for the URL and saves the response body to the file
+name provided.  The URL must have unsafe characters escaped and international
+domain names encoded.  If the file already exists, the request will includes an
+C<If-Modified-Since> header with the modification timestamp of the file.  You
+may specificy a different C<If-Modified-Since> header yourself in the C<<
+$options->{headers} >> hash.
+
+The C<success> field of the response will be true if the status code is 2XX
+or 304 (unmodified).
+
+If the file was modified and the server response includes a properly
+formatted C<Last-Modified> header, the file modification time will
+be updated accordingly.
+
+=head2 request
+
+    $response = $http->request($method, $url);
+    $response = $http->request($method, $url, \%options);
+
+Executes an HTTP request of the given method type ('GET', 'HEAD', 'PUT', etc.)
+on the given URL.  The URL must have unsafe characters escaped and
+international domain names encoded.  A hashref of options may be appended to
+modify the request.
+
+Valid options are:
+
+=over 4
+
+=item *
+
+headers
+
+A hashref containing headers to include with the request.  If the value for
+a header is an array reference, the header will be output multiple times with
+each value in the array.  These headers over-write any default headers.
+
+=item *
+
+content
+
+A scalar to include as the body of the request OR a code reference
+that will be called iteratively to produce the body of the response
+
+=item *
+
+trailer_callback
+
+A code reference that will be called if it exists to provide a hashref
+of trailing headers (only used with chunked transfer-encoding)
+
+=item *
+
+data_callback
+
+A code reference that will be called for each chunks of the response
+body received.
+
+=back
+
+If the C<content> option is a code reference, it will be called iteratively
+to provide the content body of the request.  It should return the empty
+string or undef when the iterator is exhausted.
+
+If the C<data_callback> option is provided, it will be called iteratively until
+the entire response body is received.  The first argument will be a string
+containing a chunk of the response body, the second argument will be the
+in-progress response hash reference, as described below.  (This allows
+customizing the action of the callback based on the C<status> or C<headers>
+received prior to the content body.)
+
+The C<request> method returns a hashref containing the response.  The hashref
+will have the following keys:
+
+=over 4
+
+=item *
+
+success
+
+Boolean indicating whether the operation returned a 2XX status code
+
+=item *
+
+status
+
+The HTTP status code of the response
+
+=item *
+
+reason
+
+The response phrase returned by the server
+
+=item *
+
+content
+
+The body of the response.  If the response does not have any content
+or if a data callback is provided to consume the response body,
+this will be the empty string
+
+=item *
+
+headers
+
+A hashref of header fields.  All header field names will be normalized
+to be lower case. If a header is repeated, the value will be an arrayref;
+it will otherwise be a scalar string containing the value
+
+=back
+
+On an exception during the execution of the request, the C<status> field will
+contain 599, and the C<content> field will contain the text of the exception.
+
+=for Pod::Coverage agent
+default_headers
+max_redirect
+max_size
+proxy
+timeout
+
+=head1 LIMITATIONS
+
+HTTP::Tiny is I<conditionally compliant> with the
+L<HTTP/1.1 specification|http://www.w3.org/Protocols/rfc2616/rfc2616.html>.
+It attempts to meet all "MUST" requirements of the specification, but does not
+implement all "SHOULD" requirements.
+
+Some particular limitations of note include:
+
+=over
+
+=item *
+
+HTTP::Tiny focuses on correct transport.  Users are responsible for ensuring
+that user-defined headers and content are compliant with the HTTP/1.1
+specification.
+
+=item *
+
+Users must ensure that URLs are properly escaped for unsafe characters and that
+international domain names are properly encoded to ASCII. See L<URI::Escape>,
+L<URI::_punycode> and L<Net::IDN::Encode>.
+
+=item *
+
+Redirection is very strict against the specification.  Redirection is only
+automatic for response codes 301, 302 and 307 if the request method is 'GET' or
+'HEAD'.  Response code 303 is always converted into a 'GET' redirection, as
+mandated by the specification.  There is no automatic support for status 305
+("Use proxy") redirections.
+
+=item *
+
+Persistant connections are not supported.  The C<Connection> header will
+always be set to C<close>.
+
+=item *
+
+Direct C<https> connections are supported only if L<IO::Socket::SSL> is
+installed.  There is no support for C<https> connections via proxy.
+
+=item *
+
+Cookies are not directly supported.  Users that set a C<Cookie> header
+should also set C<max_redirect> to zero to ensure cookies are not
+inappropriately re-transmitted.
+
+=item *
+
+Proxy environment variables are not supported.
+
+=item *
+
+There is no provision for delaying a request body using an C<Expect> header.
+Unexpected C<1XX> responses are silently ignored as per the specification.
+
+=item *
+
+Only 'chunked' C<Transfer-Encoding> is supported.
+
+=item *
+
+There is no support for a Request-URI of '*' for the 'OPTIONS' request.
+
+=back
+
+=head1 SEE ALSO
+
+=over 4
+
+=item *
+
+L<LWP::UserAgent>
+
+=back
+
+=head1 AUTHORS
+
+=over 4
+
+=item *
+
+Christian Hansen <chansen@cpan.org>
+
+=item *
+
+David Golden <dagolden@cpan.org>
+
+=back
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2011 by Christian Hansen.
+
+This is free software; you can redistribute it and/or modify it under
+the same terms as the Perl 5 programming language system itself.
+
+=cut
+
diff --git a/cpan/HTTP-Tiny/t/00-compile.t b/cpan/HTTP-Tiny/t/00-compile.t
new file mode 100644 (file)
index 0000000..277185d
--- /dev/null
@@ -0,0 +1,56 @@
+#!perl
+#
+# This file is part of HTTP-Tiny
+#
+# This software is copyright (c) 2011 by Christian Hansen.
+#
+# This is free software; you can redistribute it and/or modify it under
+# the same terms as the Perl 5 programming language system itself.
+#
+
+use strict;
+use warnings;
+
+use Test::More;
+
+
+
+use File::Find;
+use File::Temp qw{ tempdir };
+
+my @modules;
+find(
+  sub {
+    return if $File::Find::name !~ /\.pm\z/;
+    my $found = $File::Find::name;
+    $found =~ s{^lib/}{};
+    $found =~ s{[/\\]}{::}g;
+    $found =~ s/\.pm$//;
+    # nothing to skip
+    push @modules, $found;
+  },
+  'lib',
+);
+
+my @scripts = glob "bin/*";
+
+my $plan = scalar(@modules) + scalar(@scripts);
+$plan ? (plan tests => $plan) : (plan skip_all => "no tests to run");
+
+{
+    # fake home for cpan-testers
+     local $ENV{HOME} = tempdir( CLEANUP => 1 );
+
+    like( qx{ $^X -Ilib -e "require $_; print '$_ ok'" }, qr/^\s*$_ ok/s, "$_ loaded ok" )
+        for sort @modules;
+
+    SKIP: {
+        eval "use Test::Script 1.05; 1;";
+        skip "Test::Script needed to test script compilation", scalar(@scripts) if $@;
+        foreach my $file ( @scripts ) {
+            my $script = $file;
+            $script =~ s!.*/!!;
+            script_compiles( $file, "$script script compiles" );
+        }
+    }
+}
diff --git a/cpan/HTTP-Tiny/t/000_load.t b/cpan/HTTP-Tiny/t/000_load.t
new file mode 100644 (file)
index 0000000..b625b15
--- /dev/null
@@ -0,0 +1,21 @@
+#!perl
+#
+# This file is part of HTTP-Tiny
+#
+# This software is copyright (c) 2011 by Christian Hansen.
+#
+# This is free software; you can redistribute it and/or modify it under
+# the same terms as the Perl 5 programming language system itself.
+#
+
+use strict;
+use warnings;
+
+use Test::More tests => 1;
+
+BEGIN {
+    use_ok('HTTP::Tiny');
+}
+
+diag("HTTP::Tiny $HTTP::Tiny::VERSION, Perl $], $^X");
+
diff --git a/cpan/HTTP-Tiny/t/001_api.t b/cpan/HTTP-Tiny/t/001_api.t
new file mode 100644 (file)
index 0000000..12050a0
--- /dev/null
@@ -0,0 +1,34 @@
+#!perl
+#
+# This file is part of HTTP-Tiny
+#
+# This software is copyright (c) 2011 by Christian Hansen.
+#
+# This is free software; you can redistribute it and/or modify it under
+# the same terms as the Perl 5 programming language system itself.
+#
+
+use strict;
+use warnings;
+
+use Test::More tests => 2;
+use HTTP::Tiny;
+
+my @accessors = qw(agent default_headers max_redirect max_size proxy timeout);
+my @methods   = qw(new get request mirror);
+
+my %api;
+@api{@accessors} = (1) x @accessors;
+@api{@methods} = (1) x @accessors;
+
+can_ok('HTTP::Tiny', @methods, @accessors);
+
+my @extra =
+  grep {! $api{$_} }
+  grep { $_ !~ /\A_/ }
+  grep {; no strict 'refs'; *{"HTTP::Tiny::$_"}{CODE} }
+  sort keys %HTTP::Tiny::;
+
+ok( ! scalar @extra, "No unexpected subroutines defined" )
+  or diag "Found: @extra";
+
diff --git a/cpan/HTTP-Tiny/t/002_croakage.t b/cpan/HTTP-Tiny/t/002_croakage.t
new file mode 100644 (file)
index 0000000..317f0a7
--- /dev/null
@@ -0,0 +1,47 @@
+#!perl
+#
+# This file is part of HTTP-Tiny
+#
+# This software is copyright (c) 2011 by Christian Hansen.
+#
+# This is free software; you can redistribute it and/or modify it under
+# the same terms as the Perl 5 programming language system itself.
+#
+
+use strict;
+use warnings;
+
+use Test::More;
+use HTTP::Tiny;
+
+my %usage = (
+  'get' => q/Usage: $http->get(URL, [HASHREF])/,
+  'mirror' => q/Usage: $http->mirror(URL, FILE, [HASHREF])/,
+  'request' => q/Usage: $http->request(METHOD, URL, [HASHREF])/,
+);
+
+my @cases = (
+  ['get'],
+  ['get','http://www.example.com/','extra'],
+  ['get','http://www.example.com/','extra', 'extra'],
+  ['mirror'],
+  ['mirror','http://www.example.com/',],
+  ['mirror','http://www.example.com/','extra', 'extra'],
+  ['mirror','http://www.example.com/','extra', 'extra', 'extra'],
+  ['request'],
+  ['request','GET'],
+  ['request','GET','http://www.example.com/','extra'],
+  ['request','GET','http://www.example.com/','extra', 'extra'],
+);
+
+my $http = HTTP::Tiny->new;
+
+for my $c ( @cases ) {
+  my ($method, @args) = @$c;
+  eval {$http->$method(@args)};
+  my $err = $@;
+  like ($err, qr/\Q$usage{$method}\E/, join("|",@$c) );
+}
+
+done_testing;
+
diff --git a/cpan/HTTP-Tiny/t/010_url.t b/cpan/HTTP-Tiny/t/010_url.t
new file mode 100644 (file)
index 0000000..58ba117
--- /dev/null
@@ -0,0 +1,42 @@
+#!perl
+#
+# This file is part of HTTP-Tiny
+#
+# This software is copyright (c) 2011 by Christian Hansen.
+#
+# This is free software; you can redistribute it and/or modify it under
+# the same terms as the Perl 5 programming language system itself.
+#
+
+use strict;
+use warnings;
+
+use Test::More;
+use HTTP::Tiny;
+
+my @tests = (
+    [ 'HtTp://Example.COM/',                 'http',  'example.com',    80, '/'          ],
+    [ 'HtTp://Example.com:1024/',            'http',  'example.com',  1024, '/'          ],
+    [ 'http://example.com',                  'http',  'example.com',    80, '/'          ],
+    [ 'http://example.com:',                 'http',  'example.com',    80, '/'          ],
+    [ 'http://foo@example.com:',             'http',  'example.com',    80, '/'          ],
+    [ 'http://@example.com:',                'http',  'example.com',    80, '/'          ],
+    [ 'http://example.com?foo=bar',          'http',  'example.com',    80, '/?foo=bar'  ],
+    [ 'http://example.com?foo=bar#fragment', 'http',  'example.com',    80, '/?foo=bar'  ],
+    [ 'http://example.com/path?foo=bar',     'http',  'example.com',    80, '/path?foo=bar'  ],
+    [ 'http:///path?foo=bar',                'http',  'localhost',      80, '/path?foo=bar'  ],
+    [ 'HTTPS://example.com/',                'https', 'example.com',   443, '/'          ],
+    [ 'http://[::]:1024',                    'http',  '[::]',         1024, '/'          ],
+    [ 'xxx://foo/',                          'xxx',   'foo',         undef, '/'          ],
+);
+
+plan tests => scalar @tests;
+
+for my $test (@tests) {
+    my $url = shift(@$test);
+    my $got = [ HTTP::Tiny->_split_url($url) ];
+    my $exp = $test;
+    is_deeply($got, $exp, "->split_url('$url')");
+}
+
+
diff --git a/cpan/HTTP-Tiny/t/020_headers.t b/cpan/HTTP-Tiny/t/020_headers.t
new file mode 100644 (file)
index 0000000..3a6fc63
--- /dev/null
@@ -0,0 +1,59 @@
+#!perl
+#
+# This file is part of HTTP-Tiny
+#
+# This software is copyright (c) 2011 by Christian Hansen.
+#
+# This is free software; you can redistribute it and/or modify it under
+# the same terms as the Perl 5 programming language system itself.
+#
+
+use strict;
+use warnings;
+
+use Test::More qw[no_plan];
+use t::Util    qw[tmpfile rewind $CRLF $LF];
+use HTTP::Tiny;
+
+{
+    no warnings 'redefine';
+    sub HTTP::Tiny::Handle::can_read  { 1 };
+    sub HTTP::Tiny::Handle::can_write { 1 };
+}
+
+{
+    my $header = join $CRLF, 'Foo: Foo', 'Foo: Baz', 'Bar: Bar', '', '';
+    my $fh     = tmpfile($header);
+    my $exp    = { foo => ['Foo', 'Baz'], bar => 'Bar' };
+    my $handle = HTTP::Tiny::Handle->new(fh => $fh);
+    my $got    = $handle->read_header_lines;
+    is_deeply($got, $exp, "->read_header_lines() CRLF");
+}
+
+{
+    my $header = join $LF, 'Foo: Foo', 'Foo: Baz', 'Bar: Bar', '', '';
+    my $fh     = tmpfile($header);
+    my $exp    = { foo => ['Foo', 'Baz'], bar => 'Bar' };
+    my $handle = HTTP::Tiny::Handle->new(fh => $fh);
+    my $got    = $handle->read_header_lines;
+    is_deeply($got, $exp, "->read_header_lines() LF");
+}
+
+{
+    my $header = "Foo: $CRLF\x09Bar$CRLF\x09$CRLF\x09Baz$CRLF$CRLF";
+    my $fh     = tmpfile($header);
+    my $exp    = { foo => 'Bar Baz' };
+    my $handle = HTTP::Tiny::Handle->new(fh => $fh);
+    my $got    = $handle->read_header_lines;
+    is_deeply($got, $exp, "->read_header_lines() insane continuations");
+}
+
+{
+    my $fh      = tmpfile();
+    my $handle  = HTTP::Tiny::Handle->new(fh => $fh);
+    my $headers = { foo => ['Foo', 'Baz'], bar => 'Bar' };
+    $handle->write_header_lines($headers);
+    rewind($fh);
+    is_deeply($handle->read_header_lines, $headers, "roundtrip header lines");
+}
+
diff --git a/cpan/HTTP-Tiny/t/030_response.t b/cpan/HTTP-Tiny/t/030_response.t
new file mode 100644 (file)
index 0000000..fbeea58
--- /dev/null
@@ -0,0 +1,43 @@
+#!perl
+#
+# This file is part of HTTP-Tiny
+#
+# This software is copyright (c) 2011 by Christian Hansen.
+#
+# This is free software; you can redistribute it and/or modify it under
+# the same terms as the Perl 5 programming language system itself.
+#
+
+use strict;
+use warnings;
+
+use Test::More qw[no_plan];
+use t::Util    qw[tmpfile rewind $CRLF $LF];
+use HTTP::Tiny;
+
+sub _header {
+  return [ @{$_[0]}{qw/status reason headers protocol/} ]
+}
+
+{
+    no warnings 'redefine';
+    sub HTTP::Tiny::Handle::can_read  { 1 };
+    sub HTTP::Tiny::Handle::can_write { 1 };
+}
+
+{
+    my $response = join $CRLF, 'HTTP/1.1 200 OK', 'Foo: Foo', 'Bar: Bar', '', '';
+    my $fh       = tmpfile($response);
+    my $handle   = HTTP::Tiny::Handle->new(fh => $fh);
+    my $exp      = [ 200, 'OK', { foo => 'Foo', bar => 'Bar' }, 'HTTP/1.1' ];
+    is_deeply(_header($handle->read_response_header), $exp, "->read_response_header CRLF");
+}
+
+{
+    my $response = join $LF, 'HTTP/1.1 200 OK', 'Foo: Foo', 'Bar: Bar', '', '';
+    my $fh       = tmpfile($response);
+    my $handle   = HTTP::Tiny::Handle->new(fh => $fh);
+    my $exp      = [ 200, 'OK', { foo => 'Foo', bar => 'Bar' }, 'HTTP/1.1' ];
+    is_deeply(_header($handle->read_response_header), $exp, "->read_response_header LF");
+}
+
diff --git a/cpan/HTTP-Tiny/t/040_content.t b/cpan/HTTP-Tiny/t/040_content.t
new file mode 100644 (file)
index 0000000..e69bbed
--- /dev/null
@@ -0,0 +1,48 @@
+#!perl
+#
+# This file is part of HTTP-Tiny
+#
+# This software is copyright (c) 2011 by Christian Hansen.
+#
+# This is free software; you can redistribute it and/or modify it under
+# the same terms as the Perl 5 programming language system itself.
+#
+
+use strict;
+use warnings;
+
+use Test::More qw[no_plan];
+use t::Util    qw[tmpfile rewind $CRLF $LF];
+use HTTP::Tiny;
+
+{
+    no warnings 'redefine';
+    sub HTTP::Tiny::Handle::can_read  { 1 };
+    sub HTTP::Tiny::Handle::can_write { 1 };
+}
+
+{
+    my $chunk    = join('', '0' .. '9', 'A' .. 'Z', 'a' .. 'z', '_', $LF) x 16; # 1024
+    my $fh       = tmpfile();
+    my $handle   = HTTP::Tiny::Handle->new(fh => $fh);
+    my $nchunks  = 128;
+    my $length   = $nchunks * length $chunk;
+
+    {
+        my $request = {
+          cb => sub { $nchunks-- ? $chunk : undef },
+          headers => { 'content-length' => $length }
+        };
+        my $got = $handle->write_content_body($request);
+        is($got, $length, "written $length octets");
+    }
+
+    rewind($fh);
+
+    {
+        my $got = 0;
+        $handle->read_content_body(sub { $got += length $_[0] }, {}, $length);
+        is($got, $length, "read $length octets");
+    }
+}
+
diff --git a/cpan/HTTP-Tiny/t/050_chunked_body.t b/cpan/HTTP-Tiny/t/050_chunked_body.t
new file mode 100644 (file)
index 0000000..16c9cf7
--- /dev/null
@@ -0,0 +1,66 @@
+#!perl
+#
+# This file is part of HTTP-Tiny
+#
+# This software is copyright (c) 2011 by Christian Hansen.
+#
+# This is free software; you can redistribute it and/or modify it under
+# the same terms as the Perl 5 programming language system itself.
+#
+
+use strict;
+use warnings;
+
+use Test::More qw[no_plan];
+use t::Util    qw[tmpfile rewind $CRLF];
+use HTTP::Tiny;
+
+{
+    no warnings 'redefine';
+    sub HTTP::Tiny::Handle::can_read  { 1 };
+    sub HTTP::Tiny::Handle::can_write { 1 };
+}
+
+{
+    my $body    = join($CRLF, map { sprintf('%x', length $_) . $CRLF . $_ } 'A'..'Z', '') . $CRLF;
+    my $fh      = tmpfile($body);
+    my $handle  = HTTP::Tiny::Handle->new(fh => $fh);
+    my $exp     = ['A'..'Z'];
+    my $got     = [];
+    my $cb      = sub { push @$got, $_[0] };
+    my $response = { headers => {} };
+    $handle->read_chunked_body($cb, $response);
+    is_deeply($response->{headers}, {}, 'chunked trailers');
+    is_deeply($got, $exp, "chunked chunks");
+}
+
+{
+    my $fh      = tmpfile();
+    my $handle  = HTTP::Tiny::Handle->new(fh => $fh);
+
+    my $exp      = ['A'..'Z'];
+    my $trailers = { foo => 'Bar', bar => 'Baz' };
+    my $got      = [];
+
+    {
+        my @chunks = @$exp;
+        my $request = {
+          cb => sub { shift @chunks },
+          trailer_cb => sub { $trailers },
+        };
+        $handle->write_chunked_body($request);
+    }
+
+    rewind($fh);
+
+    {
+        my $cb = sub { push @$got, $_[0] };
+        my $response = { headers => {} };
+        $handle->read_chunked_body($cb, $response);
+        is_deeply($response->{headers}, $trailers, 'roundtrip chunked trailers');
+    }
+
+    is_deeply($got, $exp, "roundtrip chunked chunks");
+}
+
+
diff --git a/cpan/HTTP-Tiny/t/060_http_date.t b/cpan/HTTP-Tiny/t/060_http_date.t
new file mode 100644 (file)
index 0000000..bd7077a
--- /dev/null
@@ -0,0 +1,37 @@
+#!perl
+#
+# This file is part of HTTP-Tiny
+#
+# This software is copyright (c) 2011 by Christian Hansen.
+#
+# This is free software; you can redistribute it and/or modify it under
+# the same terms as the Perl 5 programming language system itself.
+#
+
+use strict;
+use warnings;
+
+use Test::More;
+use HTTP::Tiny;
+
+# test cases adapted from HTTP::Date
+my $epoch = 760233600;
+
+my @cases = (
+  ['Thu, 03 Feb 1994 00:00:00 GMT',       'RFC822+RFC1123'],
+  ['Thu,  3 Feb 1994 00:00:00 GMT',       'broken RFC822+RFC1123'],
+  ['Thursday, 03-Feb-94 00:00:00 GMT',    'old rfc850 HTTP format'],
+  ['Thursday, 03-Feb-1994 00:00:00 GMT',  'broken rfc850 HTTP format'],
+  ['Thu Feb  3 00:00:00 GMT 1994',        'ctime format'],
+  ['Thu Feb  3 00:00:00 1994',            'same as ctime, except no TZ'],
+);
+
+plan tests => 1 + @cases;
+
+is(HTTP::Tiny->_http_date($epoch), $cases[0][0], "epoch -> RFC822/RFC1123");
+
+for my $c ( @cases ) {
+  is( HTTP::Tiny->_parse_http_date($c->[0]), $epoch, $c->[1] . " -> epoch");
+}
+
+
diff --git a/cpan/HTTP-Tiny/t/100_get.t b/cpan/HTTP-Tiny/t/100_get.t
new file mode 100644 (file)
index 0000000..1d9dd91
--- /dev/null
@@ -0,0 +1,113 @@
+#!perl
+#
+# This file is part of HTTP-Tiny
+#
+# This software is copyright (c) 2011 by Christian Hansen.
+#
+# This is free software; you can redistribute it and/or modify it under
+# the same terms as the Perl 5 programming language system itself.
+#
+
+use strict;
+use warnings;
+
+use File::Basename;
+use Test::More 0.88;
+use t::Util qw[tmpfile rewind slurp monkey_patch dir_list parse_case
+  hashify connect_args set_socket_source sort_headers $CRLF $LF];
+
+use HTTP::Tiny;
+BEGIN { monkey_patch() }
+
+for my $file ( dir_list("t/cases", qr/^get/ ) ) {
+  my $label = basename($file);
+  my $data = do { local (@ARGV,$/) = $file; <> };
+  my ($params, $expect_req, $give_res) = split /--+\n/, $data;
+  my $case = parse_case($params);
+
+  my $url = $case->{url}[0];
+  my %headers = hashify( $case->{headers} );
+  my %new_args = hashify( $case->{new_args} );
+
+  my %options;
+  $options{headers} = \%headers if %headers;
+  if ( $case->{data_cb} ) {
+    $main::data = '';
+    $options{data_callback} = eval join "\n", @{$case->{data_cb}};
+    die unless ref( $options{data_callback} ) eq 'CODE';
+  }
+
+  my $version = HTTP::Tiny->VERSION || 0;
+  my $agent = $new_args{agent} || "HTTP-Tiny/$version";
+
+  # cleanup source data
+  $expect_req =~ s{HTTP-Tiny/VERSION}{$agent};
+  s{\n}{$CRLF}g for ($expect_req, $give_res);
+
+  # setup mocking and test
+  my $res_fh = tmpfile($give_res);
+  my $req_fh = tmpfile();
+
+  my $http = HTTP::Tiny->new(%new_args);
+  set_socket_source($req_fh, $res_fh);
+
+  (my $url_basename = $url) =~ s{.*/}{};
+
+  my @call_args = %options ? ($url, \%options) : ($url);
+  my $response  = $http->get(@call_args);
+
+  my ($got_host, $got_port) = connect_args();
+  my ($exp_host, $exp_port) = (
+    ($new_args{proxy} || $url ) =~ m{^http://([^:/]+?):?(\d*)/}g
+  );
+  $exp_host ||= 'localhost';
+  $exp_port ||= 80;
+
+  my $got_req = slurp($req_fh);
+
+  is ($got_host, $exp_host, "$label host $exp_host");
+  is ($got_port, $exp_port, "$label port $exp_port");
+  is( sort_headers($got_req), sort_headers($expect_req), "$label request data");
+
+  my ($rc) = $give_res =~ m{\S+\s+(\d+)}g;
+  # maybe override
+  $rc = $case->{expected_rc}[0] if defined $case->{expected_rc};
+
+  is( $response->{status}, $rc, "$label response code $rc" )
+    or diag $response->{content};
+
+  if ( substr($rc,0,1) eq '2' ) {
+    ok( $response->{success}, "$label success flag true" );
+  }
+  else {
+    ok( ! $response->{success}, "$label success flag false" );
+  }
+
+  if (defined $case->{expected_headers}) {
+    my %expected = hashify( $case->{expected_headers} );
+    is_deeply($response->{headers}, \%expected, "$label expected headers");
+  }
+
+  my $check_expected = $case->{expected_like}
+    ?  sub {
+        my ($text, $msg) = @_;
+        like( $text, "/".$case->{expected_like}[0]."/", $msg );
+      }
+    : sub {
+        my ($text, $msg) = @_;
+        my $exp_content =
+          $case->{expected} ? join("$CRLF", @{$case->{expected}}, '') : '';
+        is ( $text, $exp_content, $msg );
+      }
+    ;
+
+  if ( $options{data_callback} ) {
+    $check_expected->( $main::data, "$label cb got content" );
+    is ( $response->{content}, '', "$label resp content empty" );
+  }
+  else {
+    $check_expected->( $response->{content}, "$label content" );
+  }
+}
+
+done_testing;
diff --git a/cpan/HTTP-Tiny/t/110_mirror.t b/cpan/HTTP-Tiny/t/110_mirror.t
new file mode 100644 (file)
index 0000000..34e0c90
--- /dev/null
@@ -0,0 +1,98 @@
+#!perl
+#
+# This file is part of HTTP-Tiny
+#
+# This software is copyright (c) 2011 by Christian Hansen.
+#
+# This is free software; you can redistribute it and/or modify it under
+# the same terms as the Perl 5 programming language system itself.
+#
+
+use strict;
+use warnings;
+
+use File::Basename;
+use Test::More 0.88;
+use t::Util    qw[tmpfile rewind slurp monkey_patch dir_list parse_case
+                  set_socket_source sort_headers $CRLF $LF];
+use HTTP::Tiny;
+use File::Temp qw/tempdir/;
+
+BEGIN { monkey_patch() }
+
+my $tempdir = tempdir( TMPDIR => 1, CLEANUP => 1 );
+my $tempfile = $tempdir . "/tempfile.txt";
+
+my $known_epoch = 760233600;
+my $day = 24*3600;
+
+my %timestamp = (
+  'modified.txt'      => $known_epoch - 2 * $day,
+  'not-modified.txt'  => $known_epoch - 2 * $day,
+);
+
+for my $file ( dir_list("t/cases", qr/^mirror/ ) ) {
+  unlink $tempfile;
+  my $data = do { local (@ARGV,$/) = $file; <> };
+  my ($params, $expect_req, $give_res) = split /--+\n/, $data;
+  # cleanup source data
+  my $version = HTTP::Tiny->VERSION || 0;
+  $expect_req =~ s{VERSION}{$version};
+  s{\n}{$CRLF}g for ($expect_req, $give_res);
+
+  # figure out what request to make
+  my $case = parse_case($params);
+  my $url = $case->{url}->[0];
+  my %options;
+
+  my %headers;
+  for my $line ( @{ $case->{headers} } ) {
+    my ($k,$v) = ($line =~ m{^([^:]+): (.*)$}g);
+    $headers{$k} = $v;
+  }
+  $options{headers} = \%headers if %headers;
+
+  # maybe create a file
+  (my $url_basename = $url) =~ s{.*/}{};
+  if ( my $mtime = $timestamp{$url_basename} ) {
+    open my $fh, ">", $tempfile;
+    close $fh;
+    utime $mtime, $mtime, $tempfile;
+  }
+
+  # setup mocking and test
+  my $res_fh = tmpfile($give_res);
+  my $req_fh = tmpfile();
+
+  my $http = HTTP::Tiny->new;
+  set_socket_source($req_fh, $res_fh);
+
+  my @call_args = %options ? ($url, $tempfile, \%options) : ($url, $tempfile);
+  my $response  = $http->mirror(@call_args);
+
+  my $got_req = slurp($req_fh);
+
+  my $label = basename($file);
+
+  is( sort_headers($got_req), sort_headers($expect_req), "$label request" );
+
+  my ($rc) = $give_res =~ m{\S+\s+(\d+)}g;
+  is( $response->{status}, $rc, "$label response code $rc" )
+    or diag $response->{content};
+
+  if ( substr($rc,0,1) eq '2' ) {
+    ok( $response->{success}, "$label success flag true" );
+    ok( -e $tempfile, "$label file created" );
+  }
+  elsif ( $rc eq '304' ) {
+    ok( $response->{success}, "$label success flag true" );
+    is( (stat($tempfile))[9], $timestamp{$url_basename},
+      "$label file not overwritten" );
+  }
+  else {
+    ok( ! $response->{success}, "$label success flag false" );
+    ok( ! -e $tempfile, "$label file not created" );
+  }
+}
+
+done_testing;
diff --git a/cpan/HTTP-Tiny/t/120_put.t b/cpan/HTTP-Tiny/t/120_put.t
new file mode 100644 (file)
index 0000000..07fa27d
--- /dev/null
@@ -0,0 +1,82 @@
+#!perl
+#
+# This file is part of HTTP-Tiny
+#
+# This software is copyright (c) 2011 by Christian Hansen.
+#
+# This is free software; you can redistribute it and/or modify it under
+# the same terms as the Perl 5 programming language system itself.
+#
+
+use strict;
+use warnings;
+
+use File::Basename;
+use Test::More 0.88;
+use t::Util    qw[tmpfile rewind slurp monkey_patch dir_list parse_case
+                  set_socket_source sort_headers $CRLF $LF];
+use HTTP::Tiny;
+BEGIN { monkey_patch() }
+
+for my $file ( dir_list("t/cases", qr/^put/ ) ) {
+  my $data = do { local (@ARGV,$/) = $file; <> };
+  my ($params, $expect_req, $give_res) = split /--+\n/, $data;
+  # cleanup source data
+  my $version = HTTP::Tiny->VERSION || 0;
+  $expect_req =~ s{VERSION}{$version};
+  s{\n}{$CRLF}g for ($expect_req, $give_res);
+
+  # figure out what request to make
+  my $case = parse_case($params);
+  my $url = $case->{url}[0];
+  my %options;
+
+  my %headers;
+  for my $line ( @{ $case->{headers} } ) {
+    my ($k,$v) = ($line =~ m{^([^:]+): (.*)$}g);
+    $headers{$k} = $v;
+  }
+  $options{headers} = \%headers if %headers;
+
+  if ( $case->{content} ) {
+    $options{content} = $case->{content}[0];
+  }
+  elsif ( $case->{content_cb} ) {
+    $options{content} = eval join "\n", @{$case->{content_cb}};
+  }
+
+  if ( $case->{trailer_cb} ) {
+    $options{trailer_callback} = eval join "\n", @{$case->{trailer_cb}};
+  }
+
+  # setup mocking and test
+  my $res_fh = tmpfile($give_res);
+  my $req_fh = tmpfile();
+
+  my $http = HTTP::Tiny->new;
+  set_socket_source($req_fh, $res_fh);
+
+  (my $url_basename = $url) =~ s{.*/}{};
+
+  my @call_args = %options ? ($url, \%options) : ($url);
+  my $response  = $http->request('PUT',@call_args);
+
+  my $got_req = slurp($req_fh);
+
+  my $label = basename($file);
+
+  is( sort_headers($got_req), sort_headers($expect_req), "$label request" );
+
+  my ($rc) = $give_res =~ m{\S+\s+(\d+)}g;
+  is( $response->{status}, $rc, "$label response code $rc" )
+    or diag $response->{content};
+
+  if ( substr($rc,0,1) eq '2' ) {
+    ok( $response->{success}, "$label success flag true" );
+  }
+  else {
+    ok( ! $response->{success}, "$label success flag false" );
+  }
+}
+
+done_testing;
diff --git a/cpan/HTTP-Tiny/t/130_redirect.t b/cpan/HTTP-Tiny/t/130_redirect.t
new file mode 100644 (file)
index 0000000..9848321
--- /dev/null
@@ -0,0 +1,78 @@
+#!perl
+#
+# This file is part of HTTP-Tiny
+#
+# This software is copyright (c) 2011 by Christian Hansen.
+#
+# This is free software; you can redistribute it and/or modify it under
+# the same terms as the Perl 5 programming language system itself.
+#
+
+use strict;
+use warnings;
+
+use File::Basename;
+use Test::More 0.88;
+use t::Util qw[tmpfile rewind slurp monkey_patch dir_list parse_case
+  hashify connect_args clear_socket_source set_socket_source sort_headers
+  $CRLF $LF];
+
+use HTTP::Tiny;
+BEGIN { monkey_patch() }
+
+for my $file ( dir_list("t/cases", qr/^redirect/ ) ) {
+  my $label = basename($file);
+  my $data = do { local (@ARGV,$/) = $file; <> };
+  my ($params, @case_pairs) = split /--+\n/, $data;
+  my $case = parse_case($params);
+
+  my $url = $case->{url}[0];
+  my $method = $case->{method}[0] || 'GET';
+  my %headers = hashify( $case->{headers} );
+  my %new_args = hashify( $case->{new_args} );
+
+  my %options;
+  $options{headers} = \%headers if %headers;
+  my $call_args = %options ? [$method, $url, \%options] : [$method, $url];
+
+  my $version = HTTP::Tiny->VERSION || 0;
+  my $agent = $new_args{agent} || "HTTP-Tiny/$version";
+
+  my (@socket_pairs);
+  while ( @case_pairs ) {
+    my ($expect_req, $give_res) = splice( @case_pairs, 0, 2 );
+    # cleanup source data
+    $expect_req =~ s{HTTP-Tiny/VERSION}{$agent};
+    s{\n}{$CRLF}g for ($expect_req, $give_res);
+
+    # setup mocking and test
+    my $req_fh = tmpfile();
+    my $res_fh = tmpfile($give_res);
+
+    push @socket_pairs, [$req_fh, $res_fh, $expect_req];
+  }
+
+  clear_socket_source();
+  set_socket_source(@$_) for @socket_pairs;
+
+  my $http = HTTP::Tiny->new(%new_args);
+  my $response  = $http->request(@$call_args);
+
+  my $calls = 0
+    + (defined($new_args{max_redirect}) ? $new_args{max_redirect} : 5);
+
+  for my $i ( 0 .. $calls ) {
+    last unless @socket_pairs;
+    my ($req_fh, $res_fh, $expect_req) = @{ shift @socket_pairs };
+    my $got_req = slurp($req_fh);
+    is( sort_headers($got_req), sort_headers($expect_req), "$label request ($i)");
+    $i++;
+  }
+
+  my $exp_content = $case->{expected}
+                  ? join("$CRLF", @{$case->{expected}}) : '';
+
+  is ( $response->{content}, $exp_content, "$label content" );
+}
+
+done_testing;
diff --git a/cpan/HTTP-Tiny/t/Util.pm b/cpan/HTTP-Tiny/t/Util.pm
new file mode 100644 (file)
index 0000000..f6c5f32
--- /dev/null
@@ -0,0 +1,177 @@
+#
+# This file is part of HTTP-Tiny
+#
+# This software is copyright (c) 2011 by Christian Hansen.
+#
+# This is free software; you can redistribute it and/or modify it under
+# the same terms as the Perl 5 programming language system itself.
+#
+package t::Util;
+
+use strict;
+use warnings;
+
+use IO::File q[SEEK_SET];
+use IO::Dir;
+
+BEGIN {
+    our @EXPORT_OK = qw(
+        rewind
+        tmpfile
+        dir_list
+        slurp
+        parse_case
+        hashify
+        sort_headers
+        connect_args
+        clear_socket_source
+        set_socket_source
+        monkey_patch
+        $CRLF
+        $LF
+    );
+
+    require Exporter;
+    *import = \&Exporter::import;
+}
+
+our $CRLF = "\x0D\x0A";
+our $LF   = "\x0A";
+
+sub rewind(*) {
+    seek($_[0], 0, SEEK_SET)
+      || die(qq/Couldn't rewind file handle: '$!'/);
+}
+
+sub tmpfile {
+    my $fh = IO::File->new_tmpfile
+      || die(qq/Couldn't create a new temporary file: '$!'/);
+
+    binmode($fh)
+      || die(qq/Couldn't binmode temporary file handle: '$!'/);
+
+    if (@_) {
+        print({$fh} @_)
+          || die(qq/Couldn't write to temporary file handle: '$!'/);
+
+        seek($fh, 0, SEEK_SET)
+          || die(qq/Couldn't rewind temporary file handle: '$!'/);
+    }
+
+    return $fh;
+}
+
+sub dir_list {
+    my ($dir, $filter) = @_;
+    $filter ||= qr/./;
+    my $d = IO::Dir->new($dir)
+        or return;
+    return map { "$dir/$_" } sort grep { /$filter/ } grep { /^[^.]/ } $d->read;
+}
+
+sub slurp (*) {
+    my ($fh) = @_;
+
+    rewind($fh);
+
+    binmode($fh)
+      || die(qq/Couldn't binmode file handle: '$!'/);
+
+    my $exp = -s $fh;
+    my $buf = do { local $/; <$fh> };
+    my $got = length $buf;
+
+    ($exp == $got)
+      || die(qq[I/O read mismatch (expexted: $exp got: $got)]);
+
+    return $buf;
+}
+
+sub parse_case {
+    my ($case) = @_;
+    my %args;
+    my $key = '';
+    for my $line ( split "\n", $case ) {
+        chomp $line;
+        if ( substr($line,0,1) eq q{ } ) {
+            $line =~ s/^\s+//;
+            push @{$args{$key}}, $line;
+        }
+        else {
+            $key = $line;
+        }
+    }
+    return \%args;
+}
+
+sub hashify {
+    my ($lines) = @_;
+    return unless $lines;
+    my %hash;
+    for my $line ( @$lines ) {
+        my ($k,$v) = ($line =~ m{^([^:]+): (.*)$}g);
+        $hash{$k} = [ $hash{$k} ] if exists $hash{$k} && ref $hash{$k} ne 'ARRAY';
+        if ( ref($hash{$k}) eq 'ARRAY' ) {
+            push @{$hash{$k}}, $v;
+        }
+        else {
+            $hash{$k} = $v;
+        }
+    }
+    return %hash;
+}
+
+sub sort_headers {
+    my ($text) = shift;
+    my @lines = split /$CRLF/, $text;
+    my $request = shift(@lines) || '';
+    my @headers;
+    while (my $line = shift @lines) {
+        last unless length $line;
+        push @headers, $line;
+    }
+    @headers = sort @headers;
+    return join($CRLF, $request, @headers, '', @lines);
+}
+
+{
+    my (@req_fh, @res_fh, $monkey_host, $monkey_port);
+
+    sub clear_socket_source {
+        @req_fh = ();
+        @res_fh = ();
+    }
+
+    sub set_socket_source {
+        my ($req_fh, $res_fh) = @_;
+        push @req_fh, $req_fh;
+        push @res_fh, $res_fh;
+    }
+
+    sub connect_args { return ($monkey_host, $monkey_port) }
+
+    sub monkey_patch {
+        no warnings qw/redefine once/;
+        *HTTP::Tiny::Handle::can_read = sub {1};
+        *HTTP::Tiny::Handle::can_write = sub {1};
+        *HTTP::Tiny::Handle::connect = sub {
+            my ($self, $scheme, $host, $port) = @_;
+            $self->{host} = $monkey_host = $host;
+            $self->{port} = $monkey_port = $port;
+            $self->{fh} = shift @req_fh;
+            return $self;
+        };
+        my $original_write_request = \&HTTP::Tiny::Handle::write_request;
+        *HTTP::Tiny::Handle::write_request = sub {
+            my ($self, $request) = @_;
+            $original_write_request->($self, $request);
+            $self->{fh} = shift @res_fh;
+        };
+        *HTTP::Tiny::Handle::close = sub { 1 }; # don't close our temps
+    }
+}
+
+1;
+
+
+# vim: et ts=4 sts=4 sw=4:
diff --git a/cpan/HTTP-Tiny/t/cases/get-01.txt b/cpan/HTTP-Tiny/t/cases/get-01.txt
new file mode 100644 (file)
index 0000000..e03fb38
--- /dev/null
@@ -0,0 +1,17 @@
+url
+  http://example.com/index.html
+expected
+  abcdefghijklmnopqrstuvwxyz1234567890abcdef
+----------
+GET /index.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Content-Length: 44
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
diff --git a/cpan/HTTP-Tiny/t/cases/get-02.txt b/cpan/HTTP-Tiny/t/cases/get-02.txt
new file mode 100644 (file)
index 0000000..4b540f4
--- /dev/null
@@ -0,0 +1,22 @@
+url
+  http://example.com/index.html
+expected
+  abcdefghijklmnopqrstuvwxyz1234567890abcdef
+headers
+  Accept: */*
+  X-Custom: This is a custom header
+----------
+GET /index.html HTTP/1.1
+Host: example.com
+Accept: */*
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+X-Custom: This is a custom header
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Content-Length: 44
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
diff --git a/cpan/HTTP-Tiny/t/cases/get-03.txt b/cpan/HTTP-Tiny/t/cases/get-03.txt
new file mode 100644 (file)
index 0000000..e5eed63
--- /dev/null
@@ -0,0 +1,13 @@
+url
+  http://example.com/missing.html
+----------
+GET /missing.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 404 Not Found
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Length: 0
+
diff --git a/cpan/HTTP-Tiny/t/cases/get-04.txt b/cpan/HTTP-Tiny/t/cases/get-04.txt
new file mode 100644 (file)
index 0000000..71698f9
--- /dev/null
@@ -0,0 +1,17 @@
+url
+  http://example.com:9000/index.html
+expected
+  abcdefghijklmnopqrstuvwxyz1234567890abcdef
+----------
+GET /index.html HTTP/1.1
+Host: example.com:9000
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Content-Length: 44
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
diff --git a/cpan/HTTP-Tiny/t/cases/get-05.txt b/cpan/HTTP-Tiny/t/cases/get-05.txt
new file mode 100644 (file)
index 0000000..b689aaa
--- /dev/null
@@ -0,0 +1,21 @@
+url
+  http://example.com/chunked.html
+expected
+  abcdefghijklmnopqrstuvwxyz1234567890abcdef
+----------
+GET /chunked.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Transfer-Encoding: chunked
+
+2C
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
+
+0
+
diff --git a/cpan/HTTP-Tiny/t/cases/get-06.txt b/cpan/HTTP-Tiny/t/cases/get-06.txt
new file mode 100644 (file)
index 0000000..131bb58
--- /dev/null
@@ -0,0 +1,19 @@
+url
+  http://example.com/cb.html
+expected
+  abcdefghijklmnopqrstuvwxyz1234567890abcdef
+data_cb
+  sub { $main::data .= $_[0] }
+----------
+GET /cb.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Content-Length: 44
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
diff --git a/cpan/HTTP-Tiny/t/cases/get-07.txt b/cpan/HTTP-Tiny/t/cases/get-07.txt
new file mode 100644 (file)
index 0000000..dec18fd
--- /dev/null
@@ -0,0 +1,19 @@
+new_args
+  proxy: http://proxy.example.com:8080/
+url
+  http://example.com/index.html
+expected
+  abcdefghijklmnopqrstuvwxyz1234567890abcdef
+----------
+GET http://example.com/index.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Content-Length: 44
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
diff --git a/cpan/HTTP-Tiny/t/cases/get-08.txt b/cpan/HTTP-Tiny/t/cases/get-08.txt
new file mode 100644 (file)
index 0000000..3044db1
--- /dev/null
@@ -0,0 +1,21 @@
+new_args
+  max_size: 26
+url
+  http://example.com/index.html
+expected_rc
+  599
+expected_like
+  Size of response body exceeds the maximum allowed of 26
+----------
+GET /index.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Content-Length: 44
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
diff --git a/cpan/HTTP-Tiny/t/cases/get-09.txt b/cpan/HTTP-Tiny/t/cases/get-09.txt
new file mode 100644 (file)
index 0000000..0d5eb5d
--- /dev/null
@@ -0,0 +1,16 @@
+url
+  http://example.com/index.html
+expected
+  abcdefghijklmnopqrstuvwxyz1234567890abcdef
+----------
+GET /index.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
diff --git a/cpan/HTTP-Tiny/t/cases/get-10.txt b/cpan/HTTP-Tiny/t/cases/get-10.txt
new file mode 100644 (file)
index 0000000..23c0163
--- /dev/null
@@ -0,0 +1,21 @@
+url
+  http://example.com/chunked.html
+expected
+  abcdefghijklmnopqrstuvwxyz1234567890abcdef
+----------
+GET /chunked.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Transfer-Encoding: CHUNKED
+
+2C
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
+
+0
+
diff --git a/cpan/HTTP-Tiny/t/cases/get-11.txt b/cpan/HTTP-Tiny/t/cases/get-11.txt
new file mode 100644 (file)
index 0000000..dca6d14
--- /dev/null
@@ -0,0 +1,22 @@
+url
+  http://example.com/chunked.html
+expected
+  abcdefghijklmnopqrstuvwxyz1234567890abcdef
+----------
+GET /chunked.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Content-Length: 1024
+Transfer-Encoding: chunked
+
+2C
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
+
+0
+
diff --git a/cpan/HTTP-Tiny/t/cases/get-12.txt b/cpan/HTTP-Tiny/t/cases/get-12.txt
new file mode 100644 (file)
index 0000000..9cf2bf7
--- /dev/null
@@ -0,0 +1,17 @@
+url
+  http:///index.html
+expected
+  abcdefghijklmnopqrstuvwxyz1234567890abcdef
+----------
+GET /index.html HTTP/1.1
+Host: localhost
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Content-Length: 44
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
diff --git a/cpan/HTTP-Tiny/t/cases/get-13.txt b/cpan/HTTP-Tiny/t/cases/get-13.txt
new file mode 100644 (file)
index 0000000..7e43755
--- /dev/null
@@ -0,0 +1,21 @@
+url
+  http://example.com/chunked.html
+expected
+  abcdefghijklmnopqrstuvwxyz1234567890abcdef
+----------
+GET /chunked.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Transfer-Encoding: chunked
+
+2C; this_extension=foo
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
+
+0
+
diff --git a/cpan/HTTP-Tiny/t/cases/get-14.txt b/cpan/HTTP-Tiny/t/cases/get-14.txt
new file mode 100644 (file)
index 0000000..e232aa2
--- /dev/null
@@ -0,0 +1,22 @@
+url
+  http://example.com/index.html
+expected
+  abcdefghijklmnopqrstuvwxyz1234567890abcdef
+headers
+  X-Foo: one
+  X-Foo: two
+----------
+GET /index.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+X-Foo: one
+X-Foo: two
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Content-Length: 44
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
diff --git a/cpan/HTTP-Tiny/t/cases/get-15.txt b/cpan/HTTP-Tiny/t/cases/get-15.txt
new file mode 100644 (file)
index 0000000..0bd5065
--- /dev/null
@@ -0,0 +1,17 @@
+url
+  http://example.com/index.html
+expected
+  abcdefghijklmnopqrstuvwxyz1234567890abcdef
+----------
+GET /index.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/01.01 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Content-Length: 44
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
diff --git a/cpan/HTTP-Tiny/t/cases/get-16.txt b/cpan/HTTP-Tiny/t/cases/get-16.txt
new file mode 100644 (file)
index 0000000..e17076d
--- /dev/null
@@ -0,0 +1,19 @@
+url
+  http://example.com/index.html
+expected_rc
+  599
+expected_like
+  Malformed Status-Line
+----------
+GET /index.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/0 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Content-Length: 44
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
diff --git a/cpan/HTTP-Tiny/t/cases/get-17.txt b/cpan/HTTP-Tiny/t/cases/get-17.txt
new file mode 100644 (file)
index 0000000..e5d3d12
--- /dev/null
@@ -0,0 +1,19 @@
+url
+  http://example.com/index.html
+expected_rc
+  599
+expected_like
+  Unsupported HTTP protocol
+----------
+GET /index.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.2 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Content-Length: 44
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
diff --git a/cpan/HTTP-Tiny/t/cases/get-18.txt b/cpan/HTTP-Tiny/t/cases/get-18.txt
new file mode 100644 (file)
index 0000000..f46f48c
--- /dev/null
@@ -0,0 +1,36 @@
+url
+  http://example.com/chunked.html
+expected
+  abcdefghijklmnopqrstuvwxyz1234567890abcdef
+expected_headers
+  transfer-encoding: chunked
+  x-a: Foo
+  x-a: Bar
+  x-a: Baz
+  x-b: Foo
+  x-b: Bar
+  x-b: Baz
+  x-c: Foo
+
+----------
+GET /chunked.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 200 OK
+Transfer-Encoding: chunked
+X-A: Foo
+X-B: Foo
+X-B: Bar
+
+2C
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
+
+0
+X-A: Bar
+X-A: Baz
+X-B: Baz
+X-C: Foo
+
diff --git a/cpan/HTTP-Tiny/t/cases/get-19.txt b/cpan/HTTP-Tiny/t/cases/get-19.txt
new file mode 100644 (file)
index 0000000..2ebb9a8
--- /dev/null
@@ -0,0 +1,22 @@
+url
+  http://example.com/chunked.html
+expected
+  abcdefghijklmnopqrstuvwxyz1234567890abcdef
+----------
+GET /chunked.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Transfer-Encoding: wacky
+Transfer-Encoding: chunked
+
+2C
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
+
+0
+
diff --git a/cpan/HTTP-Tiny/t/cases/get-20.txt b/cpan/HTTP-Tiny/t/cases/get-20.txt
new file mode 100644 (file)
index 0000000..8873793
--- /dev/null
@@ -0,0 +1,21 @@
+url
+  http://example.com/index.html
+headers
+  connection: X-Foo
+  X-Foo: bar
+expected
+  abcdefghijklmnopqrstuvwxyz1234567890abcdef
+----------
+GET /index.html HTTP/1.1
+Host: example.com
+Connection: close
+X-Foo: bar
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Content-Length: 44
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
diff --git a/cpan/HTTP-Tiny/t/cases/get-21.txt b/cpan/HTTP-Tiny/t/cases/get-21.txt
new file mode 100644 (file)
index 0000000..df207f6
--- /dev/null
@@ -0,0 +1,23 @@
+url
+  http://example.com/index.html
+expected
+  abcdefghijklmnopqrstuvwxyz1234567890abcdef
+expected_rc
+  200
+----------
+GET /index.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 100 Continue
+
+HTTP/1.1 110 Arbitrary 1XX status code
+
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Content-Length: 44
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
diff --git a/cpan/HTTP-Tiny/t/cases/mirror-01.txt b/cpan/HTTP-Tiny/t/cases/mirror-01.txt
new file mode 100644 (file)
index 0000000..5ac0eb5
--- /dev/null
@@ -0,0 +1,18 @@
+url
+  http://example.com/modified.txt
+----------
+GET /modified.txt HTTP/1.1
+Host: example.com
+Connection: close
+If-Modified-Since: Tue, 01 Feb 1994 00:00:00 GMT
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Last-Modified: Wed, 02 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Content-Length: 42
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
+
diff --git a/cpan/HTTP-Tiny/t/cases/mirror-02.txt b/cpan/HTTP-Tiny/t/cases/mirror-02.txt
new file mode 100644 (file)
index 0000000..4799a2e
--- /dev/null
@@ -0,0 +1,14 @@
+url
+  http://example.com/not-modified.txt
+----------
+GET /not-modified.txt HTTP/1.1
+Host: example.com
+Connection: close
+If-Modified-Since: Tue, 01 Feb 1994 00:00:00 GMT
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 304 Not Modified
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Last-Modified: Tue, 01 Feb 1994 00:00:00 GMT
+
diff --git a/cpan/HTTP-Tiny/t/cases/mirror-03.txt b/cpan/HTTP-Tiny/t/cases/mirror-03.txt
new file mode 100644 (file)
index 0000000..711710d
--- /dev/null
@@ -0,0 +1,17 @@
+url
+  http://example.com/new.txt
+----------
+GET /new.txt HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Last-Modified: Tue, 01 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Content-Length: 42
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
+
diff --git a/cpan/HTTP-Tiny/t/cases/mirror-04.txt b/cpan/HTTP-Tiny/t/cases/mirror-04.txt
new file mode 100644 (file)
index 0000000..9beeeb3
--- /dev/null
@@ -0,0 +1,13 @@
+url
+  http://example.com/missing.txt
+----------
+GET /missing.txt HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 404 Not Found
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Length: 0
+
diff --git a/cpan/HTTP-Tiny/t/cases/mirror-05.txt b/cpan/HTTP-Tiny/t/cases/mirror-05.txt
new file mode 100644 (file)
index 0000000..f1f3573
--- /dev/null
@@ -0,0 +1,20 @@
+url
+  http://example.com/modified.txt
+headers
+  if-modified-since: Tue, 01 Feb 1994 12:00:00 GMT
+----------
+GET /modified.txt HTTP/1.1
+Host: example.com
+Connection: close
+If-Modified-Since: Tue, 01 Feb 1994 12:00:00 GMT
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Last-Modified: Wed, 02 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Content-Length: 42
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
+
diff --git a/cpan/HTTP-Tiny/t/cases/put-01.txt b/cpan/HTTP-Tiny/t/cases/put-01.txt
new file mode 100644 (file)
index 0000000..b8d6286
--- /dev/null
@@ -0,0 +1,22 @@
+url
+  http://example.com/new.txt
+headers
+  Content-Type: text/plain
+content
+  abcdefghijklmnopqrstuvwxyz1234567890abcdef
+----------
+PUT /new.txt HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+Content-Type: text/plain
+Content-Length: 42
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
+
+----------
+HTTP/1.1 201 Created
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Location: http://example.com/new.txt
+Content-Length: 0
+
diff --git a/cpan/HTTP-Tiny/t/cases/put-02.txt b/cpan/HTTP-Tiny/t/cases/put-02.txt
new file mode 100644 (file)
index 0000000..04d2675
--- /dev/null
@@ -0,0 +1,24 @@
+url
+  http://example.com/callback.txt
+headers
+  Content-Type: text/plain
+  Content-Length: 42
+content_cb
+  my @content = qq{abcdefghijklmnopqrstuvwxyz1234567890abcdef};
+  sub { shift @content }
+----------
+PUT /callback.txt HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+Content-Type: text/plain
+Content-Length: 42
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
+
+----------
+HTTP/1.1 201 Created
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Location: http://example.com/callback.txt
+Content-Length: 0
+
diff --git a/cpan/HTTP-Tiny/t/cases/put-03.txt b/cpan/HTTP-Tiny/t/cases/put-03.txt
new file mode 100644 (file)
index 0000000..99fe188
--- /dev/null
@@ -0,0 +1,25 @@
+url
+  http://example.com/chunked.txt
+headers
+  Content-Type: text/plain
+content_cb
+  my @content = qq{abcdefghijklmnopqrstuvwxyz1234567890abcdef};
+  sub { shift @content }
+----------
+PUT /chunked.txt HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+Content-Type: text/plain
+Transfer-Encoding: chunked
+
+2A
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
+0
+
+----------
+HTTP/1.1 201 Created
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Location: http://example.com/chunked.txt
+Content-Length: 0
+
diff --git a/cpan/HTTP-Tiny/t/cases/put-04.txt b/cpan/HTTP-Tiny/t/cases/put-04.txt
new file mode 100644 (file)
index 0000000..eeec295
--- /dev/null
@@ -0,0 +1,19 @@
+url
+  http://example.com/new.txt
+content
+  abcdefghijklmnopqrstuvwxyz1234567890abcdef
+----------
+PUT /new.txt HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+Content-Type: application/octet-stream
+Content-Length: 42
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
+----------
+HTTP/1.1 201 Created
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Location: http://example.com/new.txt
+Content-Length: 0
+
diff --git a/cpan/HTTP-Tiny/t/cases/put-05.txt b/cpan/HTTP-Tiny/t/cases/put-05.txt
new file mode 100644 (file)
index 0000000..f4bcaf1
--- /dev/null
@@ -0,0 +1,27 @@
+url
+  http://example.com/chunked.txt
+headers
+  Content-Type: text/plain
+content_cb
+  my @content = qq{abcdefghijklmnopqrstuvwxyz1234567890abcdef};
+  sub { shift @content }
+trailer_cb
+  sub { return { 'x-foo' => 'bar' } }
+----------
+PUT /chunked.txt HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+Content-Type: text/plain
+Transfer-Encoding: chunked
+
+2A
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
+0
+X-Foo: bar
+----------
+HTTP/1.1 201 Created
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Location: http://example.com/chunked.txt
+Content-Length: 0
+
diff --git a/cpan/HTTP-Tiny/t/cases/redirect-01.txt b/cpan/HTTP-Tiny/t/cases/redirect-01.txt
new file mode 100644 (file)
index 0000000..25e2ff2
--- /dev/null
@@ -0,0 +1,33 @@
+url
+  http://example.com/index.html
+expected
+  abcdefghijklmnopqrstuvwxyz1234567890abcdef
+----------
+GET /index.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 302 Found
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/html
+Content-Length: 53
+Location: http://example.com/index2.html
+
+<a href="http://example.com/index2.html">redirect</a>
+
+----------
+GET /index2.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Content-Length: 42
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
+
diff --git a/cpan/HTTP-Tiny/t/cases/redirect-02.txt b/cpan/HTTP-Tiny/t/cases/redirect-02.txt
new file mode 100644 (file)
index 0000000..5035879
--- /dev/null
@@ -0,0 +1,50 @@
+new_args
+  max_redirect: 0
+url
+  http://example.com/index.html
+expected
+  <a href="http://example.com/index2.html">redirect</a>
+----------
+GET /index.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 302 Found
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/html
+Content-Length: 53
+Location: http://example.com/index2.html
+
+<a href="http://example.com/index2.html">redirect</a>
+
+----------
+GET /index2.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 302 Found
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/html
+Content-Length: 53
+Location: http://example.com/index3.html
+
+<a href="http://example.com/index3.html">redirect</a>
+
+----------
+GET /index3.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Content-Length: 42
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
+
diff --git a/cpan/HTTP-Tiny/t/cases/redirect-03.txt b/cpan/HTTP-Tiny/t/cases/redirect-03.txt
new file mode 100644 (file)
index 0000000..0a7df72
--- /dev/null
@@ -0,0 +1,50 @@
+new_args
+  max_redirect: 1
+url
+  http://example.com/index.html
+expected
+  <a href="http://example.com/index3.html">redirect</a>
+----------
+GET /index.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 302 Found
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/html
+Content-Length: 53
+Location: http://example.com/index2.html
+
+<a href="http://example.com/index2.html">redirect</a>
+
+----------
+GET /index2.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 302 Found
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/html
+Content-Length: 53
+Location: http://example.com/index3.html
+
+<a href="http://example.com/index3.html">redirect</a>
+
+----------
+GET /index3.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Content-Length: 42
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
+
diff --git a/cpan/HTTP-Tiny/t/cases/redirect-04.txt b/cpan/HTTP-Tiny/t/cases/redirect-04.txt
new file mode 100644 (file)
index 0000000..c07412b
--- /dev/null
@@ -0,0 +1,50 @@
+new_args
+  max_redirect: 2
+url
+  http://example.com/index.html
+expected
+  abcdefghijklmnopqrstuvwxyz1234567890abcdef
+----------
+GET /index.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 302 Found
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/html
+Content-Length: 53
+Location: http://example.com/index2.html
+
+<a href="http://example.com/index2.html">redirect</a>
+
+----------
+GET /index2.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 302 Found
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/html
+Content-Length: 53
+Location: http://example.com/index3.html
+
+<a href="http://example.com/index3.html">redirect</a>
+
+----------
+GET /index3.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Content-Length: 42
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
+
diff --git a/cpan/HTTP-Tiny/t/cases/redirect-05.txt b/cpan/HTTP-Tiny/t/cases/redirect-05.txt
new file mode 100644 (file)
index 0000000..0691a80
--- /dev/null
@@ -0,0 +1,48 @@
+url
+  http://example.com/index.html
+expected
+  abcdefghijklmnopqrstuvwxyz1234567890abcdef
+----------
+GET /index.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 302 Found
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/html
+Content-Length: 53
+Location: http://example.com/index2.html
+
+<a href="http://example.com/index2.html">redirect</a>
+
+----------
+GET /index2.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 301 Found
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/html
+Content-Length: 53
+Location: /index3.html
+
+<a href="http://example.com/index3.html">redirect</a>
+
+----------
+GET /index3.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Content-Length: 42
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
+
diff --git a/cpan/HTTP-Tiny/t/cases/redirect-06.txt b/cpan/HTTP-Tiny/t/cases/redirect-06.txt
new file mode 100644 (file)
index 0000000..b5a6a49
--- /dev/null
@@ -0,0 +1,33 @@
+url
+  http://example.com/index.html
+expected
+  abcdefghijklmnopqrstuvwxyz1234567890abcdef
+----------
+GET /index.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 303 See Other
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/html
+Content-Length: 53
+Location: http://example.com/index2.html
+
+<a href="http://example.com/index2.html">redirect</a>
+
+----------
+GET /index2.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Content-Length: 42
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
+
diff --git a/cpan/HTTP-Tiny/t/cases/redirect-07.txt b/cpan/HTTP-Tiny/t/cases/redirect-07.txt
new file mode 100644 (file)
index 0000000..3320c6c
--- /dev/null
@@ -0,0 +1,33 @@
+url
+  http://example.com/index.html
+expected
+  abcdefghijklmnopqrstuvwxyz1234567890abcdef
+----------
+GET /index.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 307 Temporary Redirect
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/html
+Content-Length: 53
+Location: http://example.com/index2.html
+
+<a href="http://example.com/index2.html">redirect</a>
+
+----------
+GET /index2.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Content-Length: 42
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
+
diff --git a/cpan/HTTP-Tiny/t/cases/redirect-08.txt b/cpan/HTTP-Tiny/t/cases/redirect-08.txt
new file mode 100644 (file)
index 0000000..3f983b8
--- /dev/null
@@ -0,0 +1,19 @@
+url
+  http://example.com/index.html
+expected
+  <a href="http://example.com/index2.html">redirect</a>
+----------
+GET /index.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 305 Use Proxy
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/html
+Content-Length: 53
+Location: http://example.com/index2.html
+
+<a href="http://example.com/index2.html">redirect</a>
+
diff --git a/cpan/HTTP-Tiny/t/cases/redirect-09.txt b/cpan/HTTP-Tiny/t/cases/redirect-09.txt
new file mode 100644 (file)
index 0000000..02a75aa
--- /dev/null
@@ -0,0 +1,35 @@
+url
+  http://example.com/index.html
+method
+  POST
+expected
+  abcdefghijklmnopqrstuvwxyz1234567890abcdef
+----------
+POST /index.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 303 See Other
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/html
+Content-Length: 53
+Location: http://example.com/index2.html
+
+<a href="http://example.com/index2.html">redirect</a>
+
+----------
+GET /index2.html HTTP/1.1
+Host: example.com
+Connection: close
+User-Agent: HTTP-Tiny/VERSION
+
+----------
+HTTP/1.1 200 OK
+Date: Thu, 03 Feb 1994 00:00:00 GMT
+Content-Type: text/plain
+Content-Length: 42
+
+abcdefghijklmnopqrstuvwxyz1234567890abcdef
+
index a266b89..cb1ea9c 100644 (file)
 /IPC/Semaphore.pm
 /IPC/SharedMem.pm
 /IPC/SysV.pm
+/HTTP/Tiny.pm
 /JSON/PP.pm
 /JSON/PP/Boolean.pm
 /Getopt/Long.pm
index 8f861d8..d3b0fd1 100644 (file)
@@ -109,6 +109,14 @@ generation task.
 
 =item *
 
+L<HTTP::Tiny> 0.008 has been added as a dual-life module.  It is a very
+small, simple HTTP/1.1 client designed for simple GET requests and file
+mirroring.  It has has been added to enable CPAN.pm and CPANPLUS to
+"bootstrap" HTTP access to CPAN using pure Perl without relying on external
+binaries like F<curl> or F<wget>.
+
+=item *
+
 L<Module::Metadata> 1.000003 has been added as a dual-life module.  It gathers
 package and POD information from Perl module files.  It is a standalone module
 based on Module::Build::ModuleInfo for use by other module installation