This file documents the revision history for Perl extension HTTP::Body.
+1.00
+ - Added support for chunked requests if no $length value is passed to new().
+
0.9 2007-03-27 14:00:00
- Fixed bug where empty fields in multipart/form-data were ignored.
(Ton Voon)
NAME => 'HTTP::Body',
VERSION_FROM => 'lib/HTTP/Body.pm',
PREREQ_PM => {
- Carp => 0,
- File::Temp => '0.14',
- IO::File => 0,
- YAML => '0.39'
+ Carp => 0,
+ File::Temp => '0.14',
+ HTTP::Headers => 0,
+ IO::File => 0,
+ YAML => '0.39'
}
);
use Carp qw[ ];
-our $VERSION = 0.91;
+our $VERSION = 1.00;
our $TYPES = {
'application/octet-stream' => 'HTTP::Body::OctetStream',
require HTTP::Body::UrlEncoded;
require HTTP::Body::MultiPart;
+use HTTP::Headers;
+use HTTP::Message;
+
=head1 NAME
HTTP::Body - HTTP Body Parser
application/octet-stream, application/x-www-form-urlencoded, and
multipart/form-data.
+Chunked bodies are supported by not passing a length value to new().
+
It is currently used by L<Catalyst> to parse POST bodies.
=head1 METHODS
sub new {
my ( $class, $content_type, $content_length ) = @_;
- unless ( @_ == 3 ) {
- Carp::croak( $class, '->new( $content_type, $content_length )' );
+ unless ( @_ >= 2 ) {
+ Carp::croak( $class, '->new( $content_type, [ $content_length ] )' );
}
my $type;
my $self = {
buffer => '',
+ chunk_buffer => '',
body => undef,
- content_length => $content_length,
+ chunked => !defined $content_length,
+ content_length => defined $content_length ? $content_length : -1,
content_type => $content_type,
length => 0,
param => {},
sub add {
my $self = shift;
+ if ( $self->{chunked} ) {
+ $self->{chunk_buffer} .= $_[0];
+
+ while ( $self->{chunk_buffer} =~ m/^([\da-fA-F]+).*\x0D\x0A/ ) {
+ my $chunk_len = hex($1);
+
+ if ( $chunk_len == 0 ) {
+ # Strip chunk len
+ $self->{chunk_buffer} =~ s/^([\da-fA-F]+).*\x0D\x0A//;
+
+ # End of data, there may be trailing headers
+ if ( my ($headers) = $self->{chunk_buffer} =~ m/(.*)\x0D\x0A/s ) {
+ if ( my $message = HTTP::Message->parse( $headers ) ) {
+ $self->{trailing_headers} = $message->headers;
+ }
+ }
+
+ $self->{chunk_buffer} = '';
+
+ # Set content_length equal to the amount of data we read,
+ # so the spin methods can finish up.
+ $self->{content_length} = $self->{length};
+ }
+ else {
+ # Make sure we have the whole chunk in the buffer (+CRLF)
+ if ( length( $self->{chunk_buffer} ) >= $chunk_len ) {
+ # Strip chunk len
+ $self->{chunk_buffer} =~ s/^([\da-fA-F]+).*\x0D\x0A//;
+
+ # Pull chunk data out of chunk buffer into real buffer
+ $self->{buffer} .= substr $self->{chunk_buffer}, 0, $chunk_len, '';
+
+ # Strip remaining CRLF
+ $self->{chunk_buffer} =~ s/^\x0D\x0A//;
+
+ $self->{length} += $chunk_len;
+ }
+ else {
+ # Not enough data for this chunk, wait for more calls to add()
+ return;
+ }
+ }
+
+ unless ( $self->{state} eq 'done' ) {
+ $self->spin;
+ }
+ }
+
+ return;
+ }
+
my $cl = $self->content_length;
if ( defined $_[0] ) {
return $self->{body};
}
-=item buffer
+=item chunked
-read only accessor for the buffer.
+Returns 1 if the request is chunked.
=cut
-sub buffer {
- return shift->{buffer};
+sub chunked {
+ return shift->{chunked};
}
=item content_length
-read only accessor for content length
+Returns the content-length for the body data if known.
+Returns -1 if the request is chunked.
=cut
=item content_type
-read only accessor for the content type
+Returns the content-type of the body data.
=cut
=item length
-read only accessor for body length.
+Returns the total length of data we expect to read if known.
+In the case of a chunked request, returns the amount of data
+read so far.
=cut
return shift->{length};
}
+=item trailing_headers
+
+If a chunked request body had trailing headers, trailing_headers will
+return an HTTP::Headers object populated with those headers.
+
+=cut
+
+sub trailing_headers {
+ return shift->{trailing_headers};
+}
+
=item spin
Abstract method to spin the io handle.
=item state
-accessor for body state.
+Returns the current state of the parser.
=cut
=item param
-accesor for http parameters.
+Get/set body parameters.
=cut
=item upload
+Get/set file uploads.
+
=cut
sub upload {
=back
-=head1 BUGS
-
-Chunked requests are currently not supported.
-
=head1 AUTHOR
Christian Hansen, C<ch@ngmedia.com>
Sebastian Riedel, C<sri@cpan.org>
+Andy Grundman, C<andy@hybridized.org>
+
=head1 LICENSE
This library is free software. You can redistribute it and/or modify
$part->{fh} = $fh;
$part->{tempname} = $fh->filename;
- }
+ }
}
}
if ( $part->{done} ) {
if ( exists $part->{filename} ) {
- if ( $part->{filename} ne "" ) {
+ if ( $part->{filename} ne "" ) {
$part->{fh}->close;
delete @{$part}{qw[ data done fh ]};
use strict;
use warnings;
-use Test::More tests => 55;
+use Test::More tests => 60;
use Cwd;
use HTTP::Body;
my $path = catdir( getcwd(), 't', 'data', 'multipart' );
-for ( my $i = 1; $i <= 11; $i++ ) {
+for ( my $i = 1; $i <= 12; $i++ ) {
my $test = sprintf( "%.3d", $i );
my $headers = YAML::LoadFile( catfile( $path, "$test-headers.yml" ) );
is_deeply( $body->param, $results->{param}, "$test MultiPart param" );
is_deeply( $body->upload, $results->{upload}, "$test MultiPart upload" );
cmp_ok( $body->state, 'eq', 'done', "$test MultiPart state" );
- cmp_ok( $body->length, '==', $headers->{'Content-Length'}, "$test MultiPart length" );
+ cmp_ok( $body->length, '==', $body->content_length, "$test MultiPart length" );
}
use strict;
use warnings;
-use Test::More tests => 10;
+use Test::More tests => 16;
use Cwd;
+use Digest::MD5 qw(md5_hex);
use HTTP::Body;
use File::Spec::Functions;
use IO::File;
my $path = catdir( getcwd(), 't', 'data', 'urlencoded' );
-for ( my $i = 1; $i <= 2; $i++ ) {
+for ( my $i = 1; $i <= 3; $i++ ) {
my $test = sprintf( "%.3d", $i );
my $headers = YAML::LoadFile( catfile( $path, "$test-headers.yml" ) );
is_deeply( $body->param, $results->{param}, "$test UrlEncoded param" );
is_deeply( $body->upload, $results->{upload}, "$test UrlEncoded upload" );
cmp_ok( $body->state, 'eq', 'done', "$test UrlEncoded state" );
- cmp_ok( $body->length, '==', $headers->{'Content-Length'}, "$test UrlEncoded length" );
+ cmp_ok( $body->length, '==', $body->content_length, "$test UrlEncoded length" );
+
+ # Check trailing header on the chunked request
+ if ( $i == 3 ) {
+ my $content = IO::File->new( catfile( $path, "002-content.dat" ) );
+ $content->read( my $buf, 4096 );
+ is( $body->trailing_headers->header('Content-MD5'), md5_hex($buf), "$test trailing header ok" );
+ }
}
use strict;
use warnings;
-use Test::More tests => 8;
+use Test::More tests => 12;
use Cwd;
use HTTP::Body;
my $path = catdir( getcwd(), 't', 'data', 'octetstream' );
-for ( my $i = 1 ; $i <= 2 ; $i++ ) {
+for ( my $i = 1 ; $i <= 3 ; $i++ ) {
my $test = sprintf( "%.3d", $i );
my $headers = YAML::LoadFile( catfile( $path, "$test-headers.yml" ) );
cmp_ok( $body->state, 'eq', 'done', "$test UrlEncoded state" );
cmp_ok(
$body->length, '==',
- $headers->{'Content-Length'},
+ $body->content_length,
"$test UrlEncoded length"
);
}
--- /dev/null
+1ce\r
+-----------------------------7d534d1d60150\r
+Content-Disposition: form-data; name="text1"\r
+\r
+Ratione accusamus aspernatur aliquam\r
+-----------------------------7d534d1d60150\r
+Content-Disposition: form-data; name="text2"\r
+\r
+\r
+-----------------------------7d534d1d60150\r
+Content-Disposition: form-data; name="select"\r
+\r
+A\r
+-----------------------------7d534d1d60150\r
+Content-Disposition: form-data; name="select"\r
+\r
+B\r
+-----------------------------7d534d1d60150\r
+Co\r
+1ce\r
+ntent-Disposition: form-data; name="textarea"\r
+\r
+Voluptatem cumque voluptate sit recusandae at. Et quas facere rerum unde esse. Sit est et voluptatem. Vel temporibus velit neque odio non.\r
+\r
+Molestias rerum ut sapiente facere repellendus illo. Eum nulla quis aut. Quidem voluptas vitae ipsam officia voluptatibus eveniet. Aspernatur cupiditate ratione aliquam quidem corrupti. Eos sunt rerum non optio culpa.\r
+-----------------------------7d534d1d60150\r
+Content-\r
+1ce\r
+Disposition: form-data; name="upload"; filename="C:\Documents and Settings\Administrator\Desktop\hello.pl"\r
+Content-Type: text/plain\r
+\r
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+print "Hello World :)\n";
+
+\r
+-----------------------------7d534d1d60150\r
+Content-Disposition: form-data; name="upload"; filename=""\r
+Content-Type: application/octet-stream\r
+\r
+\r
+-----------------------------7d534d1d60150\r
+Content-Disposition: form-data; name="upload1"; filename="C:\\r
+1ce\r
+Documents and Settings\Administrator\Desktop\hello.pl"\r
+Content-Type: text/plain\r
+\r
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+print "Hello World :)\n";
+
+\r
+-----------------------------7d534d1d60150\r
+Content-Disposition: form-data; name="upload2"; filename="C:\Documents and Settings\Administrator\Desktop\hello.pl"\r
+Content-Type: text/plain\r
+\r
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+print "Hello World :)\n";
+
+\r
+-----------------------------7d534d1d60150-\r
+3\r
+-\r
+\r
+0\r
--- /dev/null
+---
+Transfer-Encoding: chunked
+Content-Type: multipart/form-data; boundary=---------------------------7d534d1d60150
+User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)
--- /dev/null
+---
+body: ~
+param:
+ select:
+ - A
+ - B
+ text1: Ratione accusamus aspernatur aliquam
+ text2: ""
+ textarea: "Voluptatem cumque voluptate sit recusandae at. Et quas facere rerum unde esse. Sit est et voluptatem. Vel temporibus velit neque odio non.\r\n\r\nMolestias rerum ut sapiente facere repellendus illo. Eum nulla quis aut. Quidem voluptas vitae ipsam officia voluptatibus eveniet. Aspernatur cupiditate ratione aliquam quidem corrupti. Eos sunt rerum non optio culpa."
+upload:
+ upload:
+ filename: C:\Documents and Settings\Administrator\Desktop\hello.pl
+ headers:
+ Content-Disposition: form-data; name="upload"; filename="C:\Documents and Settings\Administrator\Desktop\hello.pl"
+ Content-Type: text/plain
+ name: upload
+ size: 71
+ upload1:
+ filename: C:\Documents and Settings\Administrator\Desktop\hello.pl
+ headers:
+ Content-Disposition: form-data; name="upload1"; filename="C:\Documents and Settings\Administrator\Desktop\hello.pl"
+ Content-Type: text/plain
+ name: upload1
+ size: 71
+ upload2:
+ filename: C:\Documents and Settings\Administrator\Desktop\hello.pl
+ headers:
+ Content-Disposition: form-data; name="upload2"; filename="C:\Documents and Settings\Administrator\Desktop\hello.pl"
+ Content-Type: text/plain
+ name: upload2
+ size: 71
--- /dev/null
+143\r
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r
+146\r
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r
+d1\r
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r
+a7\r
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+\r
+0\r
--- /dev/null
+---
+Transfer-Encoding: chunked
+Content-Type: application/x.atom+xml
+User-Agent: 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312'
--- /dev/null
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
--- /dev/null
+f9\r
+text1=Ratione+accusamus+aspernatur+aliquam&text2=%C3%A5%C3%A4%C3%B6%C3%A5%C3%A4%C3%B6&select=A&select=B&textarea=Voluptatem+cumque+voluptate+sit+recusandae+at.+Et+quas+facere+rerum+unde+esse.+Sit+est+et+voluptatem.+Vel+temporibus+velit+neque+odio+no\r
+7d\r
+n.%0D%0A%0D%0AMolestias+rerum+ut+sapiente+facere+repellendus+illo.+Eum+nulla+quis+aut.+Quidem+voluptas+vitae+ipsam+officia+vo\r
+3e\r
+luptatibus+eveniet.+Aspernatur+cupiditate+ratione+aliquam+quid\r
+1f\r
+em+corrupti.+Eos+sunt+rerum+non\r
+10\r
++optio+culpa.&en\r
+8\r
+coding=f\r
+4\r
+oo%3\r
+2\r
+Db\r
+2\r
+ar\r
+0\r
+Content-MD5: 7fd07d9cb09bfd205f057440be771540\r
--- /dev/null
+---
+Transfer-Encoding: chunked
+Content-Type: application/x-www-form-urlencoded
+User-Agent: 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312'
--- /dev/null
+---
+body: ~
+param:
+ select:
+ - A
+ - B
+ text1: Ratione accusamus aspernatur aliquam
+ text2: åäöåäö
+ textarea: "Voluptatem cumque voluptate sit recusandae at. Et quas facere rerum unde esse. Sit est et voluptatem. Vel temporibus velit neque odio non.\r\n\r\nMolestias rerum ut sapiente facere repellendus illo. Eum nulla quis aut. Quidem voluptas vitae ipsam officia voluptatibus eveniet. Aspernatur cupiditate ratione aliquam quidem corrupti. Eos sunt rerum non optio culpa."
+ encoding: foo=bar
+upload: {}