require
5.005;
$VERSION
=
"5.824"
;
my
$CRLF
=
"\015\012"
;
*_bytes
=
defined
(
&utf8::downgrade
) ?
sub
{
unless
(utf8::downgrade(
$_
[0], 1)) {
Carp::croak(
"Wide character in HTTP request (bytes required)"
);
}
return
$_
[0];
}
:
sub
{
return
$_
[0];
};
sub
new {
my
$class
=
shift
;
unshift
(
@_
,
"Host"
)
if
@_
== 1;
my
%cnf
=
@_
;
my
$self
=
bless
Symbol::gensym(),
$class
;
return
$self
->http_configure(\
%cnf
);
}
sub
http_configure {
my
(
$self
,
$cnf
) =
@_
;
die
"Listen option not allowed"
if
$cnf
->{Listen};
my
$explict_host
= (
exists
$cnf
->{Host});
my
$host
=
delete
$cnf
->{Host};
my
$peer
=
$cnf
->{PeerAddr} ||
$cnf
->{PeerHost};
if
(!
$peer
) {
die
"No Host option provided"
unless
$host
;
$cnf
->{PeerAddr} =
$peer
=
$host
;
}
if
(
$peer
=~ s,:(\d+)$,,) {
$cnf
->{PeerPort} =
int
($1);
}
if
(!
$cnf
->{PeerPort}) {
$cnf
->{PeerPort} =
$self
->http_default_port;
}
if
(!
$explict_host
) {
$host
=
$peer
;
$host
=~ s/:.*//;
}
if
(
$host
&&
$host
!~ /:/) {
my
$p
=
$cnf
->{PeerPort};
$host
.=
":$p"
if
$p
!=
$self
->http_default_port;
}
$cnf
->{Proto} =
'tcp'
;
my
$keep_alive
=
delete
$cnf
->{KeepAlive};
my
$http_version
=
delete
$cnf
->{HTTPVersion};
$http_version
=
"1.1"
unless
defined
$http_version
;
my
$peer_http_version
=
delete
$cnf
->{PeerHTTPVersion};
$peer_http_version
=
"1.0"
unless
defined
$peer_http_version
;
my
$send_te
=
delete
$cnf
->{SendTE};
my
$max_line_length
=
delete
$cnf
->{MaxLineLength};
$max_line_length
= 4*1024
unless
defined
$max_line_length
;
my
$max_header_lines
=
delete
$cnf
->{MaxHeaderLines};
$max_header_lines
= 128
unless
defined
$max_header_lines
;
return
undef
unless
$self
->http_connect(
$cnf
);
$self
->host(
$host
);
$self
->keep_alive(
$keep_alive
);
$self
->send_te(
$send_te
);
$self
->http_version(
$http_version
);
$self
->peer_http_version(
$peer_http_version
);
$self
->max_line_length(
$max_line_length
);
$self
->max_header_lines(
$max_header_lines
);
${
*$self
}{
'http_buf'
} =
""
;
return
$self
;
}
sub
http_default_port {
80;
}
for
my
$method
(
qw(host keep_alive send_te max_line_length max_header_lines peer_http_version)
) {
my
$prop_name
=
"http_"
.
$method
;
no
strict
'refs'
;
*$method
=
sub
{
my
$self
=
shift
;
my
$old
= ${
*$self
}{
$prop_name
};
${
*$self
}{
$prop_name
} =
shift
if
@_
;
return
$old
;
};
}
sub
http_version {
my
$self
=
shift
;
my
$old
= ${
*$self
}{
'http_version'
};
if
(
@_
) {
my
$v
=
shift
;
$v
=
"1.0"
if
$v
eq
"1"
;
unless
(
$v
eq
"1.0"
or
$v
eq
"1.1"
) {
Carp::croak(
"Unsupported HTTP version '$v'"
);
}
${
*$self
}{
'http_version'
} =
$v
;
}
$old
;
}
sub
format_request {
my
$self
=
shift
;
my
$method
=
shift
;
my
$uri
=
shift
;
my
$content
= (
@_
% 2) ?
pop
:
""
;
for
(
$method
,
$uri
) {
Carp::croak(
"Bad method or uri"
)
if
/\s/ || !
length
;
}
push
(@{${
*$self
}{
'http_request_method'
}},
$method
);
my
$ver
= ${
*$self
}{
'http_version'
};
my
$peer_ver
= ${
*$self
}{
'http_peer_http_version'
} ||
"1.0"
;
my
@h
;
my
@connection
;
my
%given
= (
host
=> 0,
"content-length"
=> 0,
"te"
=> 0);
while
(
@_
) {
my
(
$k
,
$v
) =
splice
(
@_
, 0, 2);
my
$lc_k
=
lc
(
$k
);
if
(
$lc_k
eq
"connection"
) {
$v
=~ s/^\s+//;
$v
=~ s/\s+$//;
push
(
@connection
,
split
(/\s*,\s*/,
$v
));
next
;
}
if
(
exists
$given
{
$lc_k
}) {
$given
{
$lc_k
}++;
}
push
(
@h
,
"$k: $v"
);
}
if
(
length
(
$content
) && !
$given
{
'content-length'
}) {
push
(
@h
,
"Content-Length: "
.
length
(
$content
));
}
my
@h2
;
if
(
$given
{te}) {
push
(
@connection
,
"TE"
)
unless
grep
lc
(
$_
) eq
"te"
,
@connection
;
}
elsif
(
$self
->send_te && zlib_ok()) {
push
(
@h2
,
"TE: deflate,gzip;q=0.3"
);
push
(
@connection
,
"TE"
);
}
unless
(
grep
lc
(
$_
) eq
"close"
,
@connection
) {
if
(
$self
->keep_alive) {
if
(
$peer_ver
eq
"1.0"
) {
push
(
@h2
,
"Keep-Alive: 300"
);
unshift
(
@connection
,
"Keep-Alive"
);
}
}
else
{
push
(
@connection
,
"close"
)
if
$ver
ge
"1.1"
;
}
}
push
(
@h2
,
"Connection: "
.
join
(
", "
,
@connection
))
if
@connection
;
unless
(
$given
{host}) {
my
$h
= ${
*$self
}{
'http_host'
};
push
(
@h2
,
"Host: $h"
)
if
$h
;
}
return
_bytes(
join
(
$CRLF
,
"$method $uri HTTP/$ver"
,
@h2
,
@h
,
""
,
$content
));
}
sub
write_request {
my
$self
=
shift
;
$self
->
print
(
$self
->format_request(
@_
));
}
sub
format_chunk {
my
$self
=
shift
;
return
$_
[0]
unless
defined
(
$_
[0]) &&
length
(
$_
[0]);
return
_bytes(
sprintf
(
"%x"
,
length
(
$_
[0])) .
$CRLF
.
$_
[0] .
$CRLF
);
}
sub
write_chunk {
my
$self
=
shift
;
return
1
unless
defined
(
$_
[0]) &&
length
(
$_
[0]);
$self
->
print
(_bytes(
sprintf
(
"%x"
,
length
(
$_
[0])) .
$CRLF
.
$_
[0] .
$CRLF
));
}
sub
format_chunk_eof {
my
$self
=
shift
;
my
@h
;
while
(
@_
) {
push
(
@h
,
sprintf
"%s: %s$CRLF"
,
splice
(
@_
, 0, 2));
}
return
_bytes(
join
(
""
,
"0$CRLF"
,
@h
,
$CRLF
));
}
sub
write_chunk_eof {
my
$self
=
shift
;
$self
->
print
(
$self
->format_chunk_eof(
@_
));
}
sub
my_read {
die
if
@_
> 3;
my
$self
=
shift
;
my
$len
=
$_
[1];
for
(${
*$self
}{
'http_buf'
}) {
if
(
length
) {
$_
[0] =
substr
(
$_
, 0,
$len
,
""
);
return
length
(
$_
[0]);
}
else
{
return
$self
->
sysread
(
$_
[0],
$len
);
}
}
}
sub
my_readline {
my
$self
=
shift
;
for
(${
*$self
}{
'http_buf'
}) {
my
$max_line_length
= ${
*$self
}{
'http_max_line_length'
};
my
$pos
;
while
(1) {
$pos
=
index
(
$_
,
"\012"
);
last
if
$pos
>= 0;
die
"Line too long (limit is $max_line_length)"
if
$max_line_length
&&
length
(
$_
) >
$max_line_length
;
READ:
{
my
$n
=
$self
->
sysread
(
$_
, 1024,
length
);
unless
(
defined
$n
) {
redo
READ
if
$!{EINTR};
if
($!{EAGAIN}) {
select
(
undef
,
undef
,
undef
, 0.1);
redo
READ;
}
die
"read failed: $!"
unless
length
;
}
unless
(
$n
) {
return
undef
unless
length
;
return
substr
(
$_
, 0,
length
,
""
);
}
}
}
die
"Line too long ($pos; limit is $max_line_length)"
if
$max_line_length
&&
$pos
>
$max_line_length
;
my
$line
=
substr
(
$_
, 0,
$pos
+1,
""
);
$line
=~ s/(\015?\012)\z// ||
die
"Assert"
;
return
wantarray
? (
$line
, $1) :
$line
;
}
}
sub
_rbuf {
my
$self
=
shift
;
if
(
@_
) {
for
(${
*$self
}{
'http_buf'
}) {
my
$old
;
$old
=
$_
if
defined
wantarray
;
$_
=
shift
;
return
$old
;
}
}
else
{
return
${
*$self
}{
'http_buf'
};
}
}
sub
_rbuf_length {
my
$self
=
shift
;
return
length
${
*$self
}{
'http_buf'
};
}
sub
_read_header_lines {
my
$self
=
shift
;
my
$junk_out
=
shift
;
my
@headers
;
my
$line_count
= 0;
my
$max_header_lines
= ${
*$self
}{
'http_max_header_lines'
};
while
(
my
$line
= my_readline(
$self
)) {
if
(
$line
=~ /^(\S+?)\s*:\s*(.*)/s) {
push
(
@headers
, $1, $2);
}
elsif
(
@headers
&&
$line
=~ s/^\s+//) {
$headers
[-1] .=
" "
.
$line
;
}
elsif
(
$junk_out
) {
push
(
@$junk_out
,
$line
);
}
else
{
die
"Bad header: '$line'\n"
;
}
if
(
$max_header_lines
) {
$line_count
++;
if
(
$line_count
>=
$max_header_lines
) {
die
"Too many header lines (limit is $max_header_lines)"
;
}
}
}
return
@headers
;
}
sub
read_response_headers {
my
(
$self
,
%opt
) =
@_
;
my
$laxed
=
$opt
{laxed};
my
(
$status
,
$eol
) = my_readline(
$self
);
unless
(
defined
$status
) {
die
"Server closed connection without sending any data back"
;
}
my
(
$peer_ver
,
$code
,
$message
) =
split
(/\s+/,
$status
, 3);
if
(!
$peer_ver
||
$peer_ver
!~ s,^HTTP/,, ||
$code
!~ /^[1-5]\d\d$/) {
die
"Bad response status line: '$status'"
unless
$laxed
;
${
*$self
}{
'http_peer_http_version'
} =
"0.9"
;
${
*$self
}{
'http_status'
} =
"200"
;
substr
(${
*$self
}{
'http_buf'
}, 0, 0) =
$status
. (
$eol
||
""
);
return
200
unless
wantarray
;
return
(200,
"Assumed OK"
);
};
${
*$self
}{
'http_peer_http_version'
} =
$peer_ver
;
${
*$self
}{
'http_status'
} =
$code
;
my
$junk_out
;
if
(
$laxed
) {
$junk_out
=
$opt
{junk_out} || [];
}
my
@headers
=
$self
->_read_header_lines(
$junk_out
);
my
@te
;
my
$content_length
;
for
(
my
$i
= 0;
$i
<
@headers
;
$i
+= 2) {
my
$h
=
lc
(
$headers
[
$i
]);
if
(
$h
eq
'transfer-encoding'
) {
my
$te
=
$headers
[
$i
+1];
$te
=~ s/^\s+//;
$te
=~ s/\s+$//;
push
(
@te
,
$te
)
if
length
(
$te
);
}
elsif
(
$h
eq
'content-length'
) {
if
(
$headers
[
$i
+1] =~ /^\s*(\d{1,15})(?:\s|$)/) {
$content_length
= $1;
}
}
}
${
*$self
}{
'http_te'
} =
join
(
","
,
@te
);
${
*$self
}{
'http_content_length'
} =
$content_length
;
${
*$self
}{
'http_first_body'
}++;
delete
${
*$self
}{
'http_trailers'
};
return
$code
unless
wantarray
;
return
(
$code
,
$message
,
@headers
);
}
sub
read_entity_body {
my
$self
=
shift
;
my
$buf_ref
= \
$_
[0];
my
$size
=
$_
[1];
die
"Offset not supported yet"
if
$_
[2];
my
$chunked
;
my
$bytes
;
if
(${
*$self
}{
'http_first_body'
}) {
${
*$self
}{
'http_first_body'
} = 0;
delete
${
*$self
}{
'http_chunked'
};
delete
${
*$self
}{
'http_bytes'
};
my
$method
=
shift
(@{${
*$self
}{
'http_request_method'
}});
my
$status
= ${
*$self
}{
'http_status'
};
if
(
$method
eq
"HEAD"
) {
$bytes
= 0;
}
elsif
(
my
$te
= ${
*$self
}{
'http_te'
}) {
my
@te
=
split
(/\s*,\s*/,
lc
(
$te
));
die
"Chunked must be last Transfer-Encoding '$te'"
unless
pop
(
@te
) eq
"chunked"
;
for
(
@te
) {
if
(
$_
eq
"deflate"
&& zlib_ok()) {
my
$i
= Compress::Zlib::inflateInit();
die
"Can't make inflator"
unless
$i
;
$_
=
sub
{
scalar
(
$i
->inflate(
$_
[0])) }
}
elsif
(
$_
eq
"gzip"
&& zlib_ok()) {
my
@buf
;
$_
=
sub
{
push
(
@buf
,
$_
[0]);
return
Compress::Zlib::memGunzip(
join
(
""
,
@buf
))
if
$_
[1];
return
""
;
};
}
elsif
(
$_
eq
"identity"
) {
$_
=
sub
{
$_
[0] };
}
else
{
die
"Can't handle transfer encoding '$te'"
;
}
}
@te
=
reverse
(
@te
);
${
*$self
}{
'http_te2'
} =
@te
? \
@te
:
""
;
$chunked
= -1;
}
elsif
(
defined
(
my
$content_length
= ${
*$self
}{
'http_content_length'
})) {
$bytes
=
$content_length
;
}
elsif
(
$status
=~ /^(?:1|[23]04)/) {
$bytes
= 0;
}
else
{
}
}
else
{
$chunked
= ${
*$self
}{
'http_chunked'
};
$bytes
= ${
*$self
}{
'http_bytes'
};
}
if
(
defined
$chunked
) {
if
(
$chunked
<= 0) {
my
$line
= my_readline(
$self
);
if
(
$chunked
== 0) {
die
"Missing newline after chunk data: '$line'"
if
!
defined
(
$line
) ||
$line
ne
""
;
$line
= my_readline(
$self
);
}
die
"EOF when chunk header expected"
unless
defined
(
$line
);
my
$chunk_len
=
$line
;
$chunk_len
=~ s/;.*//;
unless
(
$chunk_len
=~ /^([\da-fA-F]+)\s*$/) {
die
"Bad chunk-size in HTTP response: $line"
;
}
$chunked
=
hex
($1);
if
(
$chunked
== 0) {
${
*$self
}{
'http_trailers'
} = [
$self
->_read_header_lines];
$$buf_ref
=
""
;
my
$n
= 0;
if
(
my
$transforms
=
delete
${
*$self
}{
'http_te2'
}) {
for
(
@$transforms
) {
$$buf_ref
=
&$_
(
$$buf_ref
, 1);
}
$n
=
length
(
$$buf_ref
);
}
delete
${
*$self
}{
'http_chunked'
};
${
*$self
}{
'http_bytes'
} = 0;
return
$n
;
}
}
my
$n
=
$chunked
;
$n
=
$size
if
$size
&&
$size
<
$n
;
$n
= my_read(
$self
,
$$buf_ref
,
$n
);
return
undef
unless
defined
$n
;
${
*$self
}{
'http_chunked'
} =
$chunked
-
$n
;
if
(
$n
> 0) {
if
(
my
$transforms
= ${
*$self
}{
'http_te2'
}) {
for
(
@$transforms
) {
$$buf_ref
=
&$_
(
$$buf_ref
, 0);
}
$n
=
length
(
$$buf_ref
);
$n
= -1
if
$n
== 0;
}
}
return
$n
;
}
elsif
(
defined
$bytes
) {
unless
(
$bytes
) {
$$buf_ref
=
""
;
return
0;
}
my
$n
=
$bytes
;
$n
=
$size
if
$size
&&
$size
<
$n
;
$n
= my_read(
$self
,
$$buf_ref
,
$n
);
return
undef
unless
defined
$n
;
${
*$self
}{
'http_bytes'
} =
$bytes
-
$n
;
return
$n
;
}
else
{
$size
||= 8*1024;
return
my_read(
$self
,
$$buf_ref
,
$size
);
}
}
sub
get_trailers {
my
$self
=
shift
;
@{${
*$self
}{
'http_trailers'
} || []};
}
BEGIN {
my
$zlib_ok
;
sub
zlib_ok {
return
$zlib_ok
if
defined
$zlib_ok
;
local
$@;
local
$SIG
{__DIE__};
$zlib_ok
= 0;
eval
{
Compress::Zlib->VERSION(1.10);
$zlib_ok
++;
};
return
$zlib_ok
;
}
}
1;