--- AnyEvent-HTTP/HTTP.pm 2011/01/01 02:20:49 1.76 +++ AnyEvent-HTTP/HTTP.pm 2022/08/05 20:48:14 1.139 @@ -17,7 +17,7 @@ This module implements a simple, stateless and non-blocking HTTP client. It supports GET, POST and other request methods, cookies and more, -all on a very low level. It can follow redirects supports proxies and +all on a very low level. It can follow redirects, supports proxies, and automatically limits the number of connections to the values specified in the RFC. @@ -38,8 +38,7 @@ package AnyEvent::HTTP; -use strict; -no warnings; +use common::sense; use Errno (); @@ -49,24 +48,20 @@ use base Exporter::; -our $VERSION = '1.5'; +our $VERSION = 2.25; our @EXPORT = qw(http_get http_post http_head http_request); our $USERAGENT = "Mozilla/5.0 (compatible; U; AnyEvent-HTTP/$VERSION; +http://software.schmorp.de/pkg/AnyEvent)"; our $MAX_RECURSE = 10; -our $MAX_PERSISTENT = 8; -our $PERSISTENT_TIMEOUT = 2; +our $PERSISTENT_TIMEOUT = 3; our $TIMEOUT = 300; - -# changing these is evil -our $MAX_PERSISTENT_PER_HOST = 0; -our $MAX_PER_HOST = 4; +our $MAX_PER_HOST = 4; # changing this is evil our $PROXY; our $ACTIVE = 0; -my %KA_COUNT; # number of open keep-alive connections per host +my %KA_CACHE; # indexed by uhost currently, points to [$handle...] array my %CO_SLOT; # number of open connections, and wait queue, per host =item http_get $url, key => value..., $cb->($data, $headers) @@ -96,7 +91,7 @@ destroyed before the callback is called, the request will be cancelled. The callback will be called with the response body data as first argument -(or C if an error occured), and a hash-ref with response headers +(or C if an error occurred), and a hash-ref with response headers (and trailers) as second argument. All the headers in that hash are lowercased. In addition to the response @@ -124,9 +119,23 @@ joined together with a comma (C<,>), as per the HTTP spec. If an internal error occurs, such as not being able to resolve a hostname, -then C<$data> will be C, C<< $headers->{Status} >> will be C<59x> -(usually C<599>) and the C pseudo-header will contain an error -message. +then C<$data> will be C, C<< $headers->{Status} >> will be +C<590>-C<599> and the C pseudo-header will contain an error +message. Currently the following status codes are used: + +=over 4 + +=item 595 - errors during connection establishment, proxy handshake. + +=item 596 - errors during TLS negotiation, request sending and header processing. + +=item 597 - errors during body receiving or processing. + +=item 598 - user aborted request via C or C. + +=item 599 - other, usually nonretryable, errors (garbled URL etc.). + +=back A typical callback might look like this: @@ -147,8 +156,13 @@ =item recurse => $count (default: $MAX_RECURSE) -Whether to recurse requests or not, e.g. on redirects, authentication -retries and so on, and how often to do so. +Whether to recurse requests or not, e.g. on redirects, authentication and +other retries and so on, and how often to do so. + +Only redirects to http and https URLs are supported. While most common +redirection forms are handled entirely within this module, some require +the use of the optional L module. If it is required but missing, then +the request will fail with an error. =item headers => hashref @@ -158,6 +172,13 @@ (this can be suppressed by using C for these headers in which case they won't be sent at all). +You really should provide your own C header value that is +appropriate for your program - I wouldn't be surprised if the default +AnyEvent string gets blocked by webservers sooner or later. + +Also, make sure that your headers names and values do not contain any +embedded newlines. + =item timeout => $seconds The time-out to use for various stages - each connect attempt will reset @@ -168,11 +189,21 @@ =item proxy => [$host, $port[, $scheme]] or undef -Use the given http proxy for all requests. If not specified, then the -default proxy (as specified by C<$ENV{http_proxy}>) is used. +Use the given http proxy for all requests, or no proxy if C is +used. -C<$scheme> must be either missing, C for HTTP or C for -HTTPS. +C<$scheme> must be either missing or must be C for HTTP. + +If not specified, then the default proxy is used (see +C). + +Currently, if your proxy requires authorization, you have to specify an +appropriate "Proxy-Authorization" header in every request. + +Note that this module will prefer an existing persistent connection, +even if that connection was made using another proxy. If you need to +ensure that a new connection is made in this case, you can either force +C to false or e.g. use the proxy address in your C. =item body => $string @@ -184,14 +215,16 @@ Passing this parameter enables (simplified) cookie-processing, loosely based on the original netscape specification. -The C<$hash_ref> must be an (initially empty) hash reference which will -get updated automatically. It is possible to save the cookie jar to -persistent storage with something like JSON or Storable, but this is not -recommended, as session-only cookies might survive longer than expected. +The C<$hash_ref> must be an (initially empty) hash reference which +will get updated automatically. It is possible to save the cookie jar +to persistent storage with something like JSON or Storable - see the +C function if you wish to remove +expired or session-only cookies, and also for documentation on the format +of the cookie jar. Note that this cookie implementation is not meant to be complete. If you want complete cookie management you have to do that on your -own. C is meant as a quick fix to get some cookie-using sites +own. C is meant as a quick fix to get most cookie-using sites working. Cookies are a privacy disaster, do not use them unless required to. @@ -211,10 +244,22 @@ The default for this option is C, which could be interpreted as "give me the page, no matter what". +See also the C parameter. + +=item sessionid => $string + +The module might reuse connections to the same host internally (regardless +of other settings, such as C or C). Sometimes (e.g. +when using TLS or a specfic proxy), you do not want to reuse connections +from other sessions. This can be achieved by setting this parameter to +some unique ID (such as the address of an object storing your state data +or the TLS context, or the proxy IP) - only connections using the same +unique ID will be reused. + =item on_prepare => $callback->($fh) In rare cases you need to "tune" the socket before it is used to -connect (for exmaple, to bind it on a given IP address). This parameter +connect (for example, to bind it on a given IP address). This parameter overrides the prepare callback passed to C and behaves exactly the same way (e.g. it has to provide a timeout). See the description for the C<$prepare_cb> argument of @@ -228,6 +273,12 @@ obviously, it has to follow the same calling conventions, except that it may always return a connection guard object. +The connections made by this hook will be treated as equivalent to +connections made the built-in way, specifically, they will be put into +and taken from the persistent connection cache. If your C<$tcp_connect> +function is incompatible with this kind of re-use, consider switching off +C connections and/or providing a C identifier. + There are probably lots of weird uses for this function, starting from tracing the hosts C actually tries to connect, to (inexact but fast) host => IP address caching or even socks protocol support. @@ -289,10 +340,10 @@ connection. In error cases, C will be passed. When there is no body (e.g. status C<304>), the empty string will be passed. -The handle object might or might not be in TLS mode, might be connected to -a proxy, be a persistent connection etc., and configured in unspecified -ways. The user is responsible for this handle (it will not be used by this -module anymore). +The handle object might or might not be in TLS mode, might be connected +to a proxy, be a persistent connection, use chunked transfer encoding +etc., and configured in unspecified ways. The user is responsible for this +handle (it will not be used by this module anymore). This is useful with some push-type services, where, after the initial headers, an interactive protocol is used (typical example would be the @@ -301,6 +352,47 @@ If you think you need this, first have a look at C, to see if that doesn't solve your problem in a better way. +=item persistent => $boolean + +Try to create/reuse a persistent connection. When this flag is set +(default: true for idempotent requests, false for all others), then +C tries to re-use an existing (previously-created) +persistent connection to same host (i.e. identical URL scheme, hostname, +port and sessionid) and, failing that, tries to create a new one. + +Requests failing in certain ways will be automatically retried once, which +is dangerous for non-idempotent requests, which is why it defaults to off +for them. The reason for this is because the bozos who designed HTTP/1.1 +made it impossible to distinguish between a fatal error and a normal +connection timeout, so you never know whether there was a problem with +your request or not. + +When reusing an existent connection, many parameters (such as TLS context) +will be ignored. See the C parameter for a workaround. + +=item keepalive => $boolean + +Only used when C is also true. This parameter decides whether +C tries to handshake a HTTP/1.0-style keep-alive connection +(as opposed to only a HTTP/1.1 persistent connection). + +The default is true, except when using a proxy, in which case it defaults +to false, as HTTP/1.0 proxies cannot support this in a meaningful way. + +=item handle_params => { key => value ... } + +The key-value pairs in this hash will be passed to any L +constructor that is called - not all requests will create a handle, and +sometimes more than one is created, so this parameter is only good for +setting hints. + +Example: set the maximum read size to 4096, to potentially conserve memory +at the cost of speed. + + handle_params => { + max_read_size => 4096, + }, + =back Example: do a simple HTTP GET request for http://www.nethype.de/ and print @@ -315,7 +407,8 @@ timeout of 30 seconds. http_request - GET => "https://www.google.com", + HEAD => "https://www.google.com", + headers => { "user-agent" => "MySearchClient 1.0" }, timeout => 30, sub { my ($body, $hdr) = @_; @@ -336,6 +429,9 @@ =cut +############################################################################# +# wait queue/slots + sub _slot_schedule; sub _slot_schedule($) { my $host = shift; @@ -366,36 +462,66 @@ _slot_schedule $_[0]; } +############################################################################# +# cookie handling + +# expire cookies +sub cookie_jar_expire($;$) { + my ($jar, $session_end) = @_; + + %$jar = () if $jar->{version} != 2; + + my $anow = AE::now; + + while (my ($chost, $paths) = each %$jar) { + next unless ref $paths; + + while (my ($cpath, $cookies) = each %$paths) { + while (my ($cookie, $kv) = each %$cookies) { + if (exists $kv->{_expires}) { + delete $cookies->{$cookie} + if $anow > $kv->{_expires}; + } elsif ($session_end) { + delete $cookies->{$cookie}; + } + } + + delete $paths->{$cpath} + unless %$cookies; + } + + delete $jar->{$chost} + unless %$paths; + } +} + # extract cookies from jar sub cookie_jar_extract($$$$) { - my ($jar, $uscheme, $uhost, $upath) = @_; + my ($jar, $scheme, $host, $path) = @_; + + %$jar = () if $jar->{version} != 2; - %$jar = () if $jar->{version} != 1; + $host = AnyEvent::Util::idn_to_ascii $host + if $host =~ /[^\x00-\x7f]/; my @cookies; while (my ($chost, $paths) = each %$jar) { next unless ref $paths; - if ($chost =~ /^\./) { - next unless $chost eq substr $uhost, -length $chost; - } elsif ($chost =~ /\./) { - next unless $chost eq $uhost; - } else { - next; - } + # exact match or suffix including . match + $chost eq $host or ".$chost" eq substr $host, -1 - length $chost + or next; while (my ($cpath, $cookies) = each %$paths) { - next unless $cpath eq substr $upath, 0, length $cpath; + next unless $cpath eq substr $path, 0, length $cpath; while (my ($cookie, $kv) = each %$cookies) { - next if $uscheme ne "https" && exists $kv->{secure}; + next if $scheme ne "https" && exists $kv->{secure}; - if (exists $kv->{expires}) { - if (AE::now > parse_date ($kv->{expires})) { - delete $cookies->{$cookie}; - next; - } + if (exists $kv->{_expires} and AE::now > $kv->{_expires}) { + delete $cookies->{$cookie}; + next; } my $value = $kv->{value}; @@ -414,36 +540,43 @@ } # parse set_cookie header into jar -sub cookie_jar_set_cookie($$$) { - my ($jar, $set_cookie, $uhost) = @_; +sub cookie_jar_set_cookie($$$$) { + my ($jar, $set_cookie, $host, $date) = @_; + + %$jar = () if $jar->{version} != 2; + + my $anow = int AE::now; + my $snow; # server-now for ($set_cookie) { # parse NAME=VALUE my @kv; + # expires is not http-compliant in the original cookie-spec, + # we support the official date format and some extensions while ( m{ \G\s* (?: - expires \s*=\s* ([A-Z][a-z][a-z],\ [^,;]+) - | ([^=;,[:space:]]+) \s*=\s* (?: "((?:[^\\"]+|\\.)*)" | ([^=;,[:space:]]*) ) + expires \s*=\s* ([A-Z][a-z][a-z]+,\ [^,;]+) + | ([^=;,[:space:]]+) (?: \s*=\s* (?: "((?:[^\\"]+|\\.)*)" | ([^;,[:space:]]*) ) )? ) }gcxsi ) { my $name = $2; my $value = $4; - unless (defined $name) { + if (defined $1) { # expires $name = "expires"; $value = $1; - } elsif (!defined $value) { + } elsif (defined $3) { # quoted $value = $3; $value =~ s/\\(.)/$1/gs; } - push @kv, lc $name, $value; + push @kv, @kv ? lc $name : $name, $value; last unless /\G\s*;/gc; } @@ -453,14 +586,20 @@ my $name = shift @kv; my %kv = (value => shift @kv, @kv); - $kv{expires} ||= format_date (AE::now + $kv{"max-age"}) - if exists $kv{"max-age"}; + if (exists $kv{"max-age"}) { + $kv{_expires} = $anow + delete $kv{"max-age"}; + } elsif (exists $kv{expires}) { + $snow ||= parse_date ($date) || $anow; + $kv{_expires} = $anow + (parse_date (delete $kv{expires}) - $snow); + } else { + delete $kv{_expires}; + } my $cdom; my $cpath = (delete $kv{path}) || "/"; if (exists $kv{domain}) { - $cdom = delete $kv{domain}; + $cdom = $kv{domain}; $cdom =~ s/^\.?/./; # make sure it starts with a "." @@ -469,20 +608,64 @@ # this is not rfc-like and not netscape-like. go figure. my $ndots = $cdom =~ y/.//; next if $ndots < ($cdom =~ /\.[^.][^.]\.[^.][^.]$/ ? 3 : 2); + + $cdom = substr $cdom, 1; # remove initial . } else { - $cdom = $uhost; + $cdom = $host; } # store it - $jar->{version} = 1; - $jar->{$cdom}{$cpath}{$name} = \%kv; + $jar->{version} = 2; + $jar->{lc $cdom}{$cpath}{$name} = \%kv; redo if /\G\s*,/gc; } } +############################################################################# +# keepalive/persistent connection cache + +# fetch a connection from the keepalive cache +sub ka_fetch($) { + my $ka_key = shift; + + my $hdl = pop @{ $KA_CACHE{$ka_key} }; # currently we reuse the MOST RECENTLY USED connection + delete $KA_CACHE{$ka_key} + unless @{ $KA_CACHE{$ka_key} }; + + $hdl +} + +sub ka_store($$) { + my ($ka_key, $hdl) = @_; + + my $kaa = $KA_CACHE{$ka_key} ||= []; + + my $destroy = sub { + my @ka = grep $_ != $hdl, @{ $KA_CACHE{$ka_key} }; + + $hdl->destroy; + + @ka + ? $KA_CACHE{$ka_key} = \@ka + : delete $KA_CACHE{$ka_key}; + }; + + # on error etc., destroy + $hdl->on_error ($destroy); + $hdl->on_eof ($destroy); + $hdl->on_read ($destroy); + $hdl->timeout ($PERSISTENT_TIMEOUT); + + push @$kaa, $hdl; + shift @$kaa while @$kaa > $MAX_PER_HOST; +} + +############################################################################# +# utilities + # continue to parse $_ for headers and place them into the arg -sub parse_hdr() { +sub _parse_hdr() { my %hdr; # things seen, not parsed: @@ -506,11 +689,72 @@ \%hdr } +############################################################################# +# http_get + our $qr_nlnl = qr{(? 1, sslv2 => 1 }; our $TLS_CTX_HIGH = { cache => 1, verify => 1, verify_peername => "https" }; +# maybe it should just become a normal object :/ + +sub _destroy_state(\%) { + my ($state) = @_; + + $state->{handle}->destroy if $state->{handle}; + %$state = (); +} + +sub _error(\%$$) { + my ($state, $cb, $hdr) = @_; + + &_destroy_state ($state); + + $cb->(undef, $hdr); + () +} + +our %IDEMPOTENT = ( + DELETE => 1, + GET => 1, + QUERY => 1, + HEAD => 1, + OPTIONS => 1, + PUT => 1, + TRACE => 1, + + ACL => 1, + "BASELINE-CONTROL" => 1, + BIND => 1, + CHECKIN => 1, + CHECKOUT => 1, + COPY => 1, + LABEL => 1, + LINK => 1, + MERGE => 1, + MKACTIVITY => 1, + MKCALENDAR => 1, + MKCOL => 1, + MKREDIRECTREF => 1, + MKWORKSPACE => 1, + MOVE => 1, + ORDERPATCH => 1, + PRI => 1, + PROPFIND => 1, + PROPPATCH => 1, + REBIND => 1, + REPORT => 1, + SEARCH => 1, + UNBIND => 1, + UNCHECKOUT => 1, + UNLINK => 1, + UNLOCK => 1, + UPDATE => 1, + UPDATEREDIRECTREF => 1, + "VERSION-CONTROL" => 1, +); + sub http_request($$@) { my $cb = pop; my ($method, $url, %arg) = @_; @@ -537,11 +781,11 @@ return $cb->(undef, { @pseudo, Status => 599, Reason => "Too many redirections" }) if $recurse < 0; - my $proxy = $arg{proxy} || $PROXY; + my $proxy = exists $arg{proxy} ? $arg{proxy} : $PROXY; my $timeout = $arg{timeout} || $TIMEOUT; - my ($uscheme, $uauthority, $upath, $query, $fragment) = - $url =~ m|(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:(\?[^#]*))?(?:#(.*))?|; + my ($uscheme, $uauthority, $upath, $query, undef) = # ignore fragment + $url =~ m|^([^:]+):(?://([^/?#]*))?([^?#]*)(?:(\?[^#]*))?(?:#(.*))?$|; $uscheme = lc $uscheme; @@ -549,10 +793,10 @@ : $uscheme eq "https" ? 443 : return $cb->(undef, { @pseudo, Status => 599, Reason => "Only http and https URL schemes supported" }); - $uauthority =~ /^(?: .*\@ )? ([^\@:]+) (?: : (\d+) )?$/x + $uauthority =~ /^(?: .*\@ )? ([^\@]+?) (?: : (\d+) )?$/x or return $cb->(undef, { @pseudo, Status => 599, Reason => "Unparsable URL" }); - my $uhost = $1; + my $uhost = lc $1; $uport = $2 if defined $2; $hdr{host} = defined $2 ? "$uhost:$2" : "$uhost" @@ -581,6 +825,9 @@ # don't support https requests over https-proxy transport, # can't be done with tls as spec'ed, unless you double-encrypt. $rscheme = "http" if $uscheme eq "https" && $rscheme eq "https"; + + $rhost = lc $rhost; + $rscheme = lc $rscheme; } else { ($rhost, $rport, $rscheme, $rpath) = ($uhost, $uport, $uscheme, $upath); } @@ -592,316 +839,421 @@ $hdr{"content-length"} = length $arg{body} if length $arg{body} || $method ne "GET"; - $hdr{connection} = "close TE"; #1.1 - $hdr{te} = "trailers" unless exists $hdr{te}; #1.1 + my $idempotent = $IDEMPOTENT{$method}; - my %state = (connect_guard => 1); + # default value for keepalive is true iff the request is for an idempotent method + my $persistent = exists $arg{persistent} ? !!$arg{persistent} : $idempotent; + my $keepalive = exists $arg{keepalive} ? !!$arg{keepalive} : !$proxy; + my $was_persistent; # true if this is actually a recycled connection - _get_slot $uhost, sub { - $state{slot_guard} = shift; + # the key to use in the keepalive cache + my $ka_key = "$uscheme\x00$uhost\x00$uport\x00$arg{sessionid}"; - return unless $state{connect_guard}; + $hdr{connection} = ($persistent ? $keepalive ? "keep-alive, " : "" : "close, ") . "Te"; #1.1 + $hdr{te} = "trailers" unless exists $hdr{te}; #1.1 - my $connect_cb = sub { - $state{fh} = shift - or do { - my $err = "$!"; - %state = (); - return $cb->(undef, { @pseudo, Status => 599, Reason => $err }); - }; + my %state = (connect_guard => 1); - return unless delete $state{connect_guard}; + my $ae_error = 595; # connecting - # get handle - $state{handle} = new AnyEvent::Handle - fh => $state{fh}, - peername => $rhost, - tls_ctx => $arg{tls_ctx}, - # these need to be reconfigured on keepalive handles - timeout => $timeout, - on_error => sub { - %state = (); - $cb->(undef, { @pseudo, Status => 599, Reason => $_[2] }); - }, - on_eof => sub { - %state = (); - $cb->(undef, { @pseudo, Status => 599, Reason => "Unexpected end-of-file" }); - }, - ; + # handle actual, non-tunneled, request + my $handle_actual_request = sub { + $ae_error = 596; # request phase + + my $hdl = $state{handle}; + + $hdl->starttls ("connect") if $uscheme eq "https" && !exists $hdl->{tls}; + + # send request + $hdl->push_write ( + "$method $rpath HTTP/1.1\015\012" + . (join "", map "\u$_: $hdr{$_}\015\012", grep defined $hdr{$_}, keys %hdr) + . "\015\012" + . $arg{body} + ); + + # return if error occurred during push_write() + return unless %state; + + # reduce memory usage, save a kitten, also re-use it for the response headers. + %hdr = (); + + # status line and headers + $state{read_response} = sub { + return unless %state; + + for ("$_[1]") { + y/\015//d; # weed out any \015, as they show up in the weirdest of places. + + /^HTTP\/0*([0-9\.]+) \s+ ([0-9]{3}) (?: \s+ ([^\012]*) )? \012/gxci + or return _error %state, $cb, { @pseudo, Status => 599, Reason => "Invalid server response" }; + + # 100 Continue handling + # should not happen as we don't send expect: 100-continue, + # but we handle it just in case. + # since we send the request body regardless, if we get an error + # we are out of-sync, which we currently do NOT handle correctly. + return $state{handle}->push_read (line => $qr_nlnl, $state{read_response}) + if $2 eq 100; + + push @pseudo, + HTTPVersion => $1, + Status => $2, + Reason => $3, + ; - # limit the number of persistent connections - # keepalive not yet supported -# if ($KA_COUNT{$_[1]} < $MAX_PERSISTENT_PER_HOST) { -# ++$KA_COUNT{$_[1]}; -# $state{handle}{ka_count_guard} = AnyEvent::Util::guard { -# --$KA_COUNT{$_[1]} -# }; -# $hdr{connection} = "keep-alive"; -# } - - $state{handle}->starttls ("connect") if $rscheme eq "https"; - - # handle actual, non-tunneled, request - my $handle_actual_request = sub { - $state{handle}->starttls ("connect") if $uscheme eq "https" && !exists $state{handle}{tls}; - - # send request - $state{handle}->push_write ( - "$method $rpath HTTP/1.1\015\012" - . (join "", map "\u$_: $hdr{$_}\015\012", grep defined $hdr{$_}, keys %hdr) - . "\015\012" - . (delete $arg{body}) - ); + my $hdr = _parse_hdr + or return _error %state, $cb, { @pseudo, Status => 599, Reason => "Garbled response headers" }; - # return if error occured during push_write() - return unless %state; + %hdr = (%$hdr, @pseudo); + } - %hdr = (); # reduce memory usage, save a kitten, also make it possible to re-use + # redirect handling + # relative uri handling forced by microsoft and other shitheads. + # we give our best and fall back to URI if available. + if (exists $hdr{location}) { + my $loc = $hdr{location}; + + if ($loc =~ m%^//%) { # // + $loc = "$uscheme:$loc"; + + } elsif ($loc eq "") { + $loc = $url; + + } elsif ($loc !~ /^(?: $ | [^:\/?\#]+ : )/x) { # anything "simple" + $loc =~ s/^\.\/+//; + + if ($loc !~ m%^[.?#]%) { + my $prefix = "$uscheme://$uauthority"; + + unless ($loc =~ s/^\///) { + $prefix .= $upath; + $prefix =~ s/\/[^\/]*$//; + } - # status line and headers - $state{read_response} = sub { - for ("$_[1]") { - y/\015//d; # weed out any \015, as they show up in the weirdest of places. - - /^HTTP\/0*([0-9\.]+) \s+ ([0-9]{3}) (?: \s+ ([^\012]*) )? \012/gxci - or return (%state = (), $cb->(undef, { @pseudo, Status => 599, Reason => "Invalid server response" })); - - # 100 Continue handling - # should not happen as we don't send expect: 100-continue, - # but we handle it just in case. - # since we send the request body regardless, if we get an error - # we are out of-sync, which we currently do NOT handle correctly. - return $state{handle}->push_read (line => $qr_nlnl, $state{read_response}) - if $2 eq 100; - - push @pseudo, - HTTPVersion => $1, - Status => $2, - Reason => $3, - ; + $loc = "$prefix/$loc"; - my $hdr = parse_hdr - or return (%state = (), $cb->(undef, { @pseudo, Status => 599, Reason => "Garbled response headers" })); + } elsif (eval { require URI }) { # uri + $loc = URI->new_abs ($loc, $url)->as_string; - %hdr = (%$hdr, @pseudo); + } else { + return _error %state, $cb, { @pseudo, Status => 599, Reason => "Cannot parse Location (URI module missing)" }; + #$hdr{Status} = 599; + #$hdr{Reason} = "Unparsable Redirect (URI module missing)"; + #$recurse = 0; } + } - # redirect handling - # microsoft and other shitheads don't give a shit for following standards, - # try to support some common forms of broken Location headers. - if ($hdr{location} !~ /^(?: $ | [^:\/?\#]+ : )/x) { - $hdr{location} =~ s/^\.\/+//; - - my $url = "$rscheme://$uhost:$uport"; - - unless ($hdr{location} =~ s/^\///) { - $url .= $upath; - $url =~ s/\/[^\/]*$//; - } + $hdr{location} = $loc; + } - $hdr{location} = "$url/$hdr{location}"; - } + my $redirect; - my $redirect; + if ($recurse) { + my $status = $hdr{Status}; - if ($recurse) { - my $status = $hdr{Status}; + # industry standard is to redirect POST as GET for + # 301, 302 and 303, in contrast to HTTP/1.0 and 1.1. + # also, the UA should ask the user for 301 and 307 and POST, + # industry standard seems to be to simply follow. + # we go with the industry standard. 308 is defined + # by rfc7538 + if ($status == 301 or $status == 302 or $status == 303) { + $redirect = 1; + # HTTP/1.1 is unclear on how to mutate the method + unless ($method eq "HEAD") { + $method = "GET"; + delete $arg{body}; + } + } elsif ($status == 307 or $status == 308) { + $redirect = 1; + } + } - # industry standard is to redirect POST as GET for - # 301, 302 and 303, in contrast to HTTP/1.0 and 1.1. - # also, the UA should ask the user for 301 and 307 and POST, - # industry standard seems to be to simply follow. - # we go with the industry standard. - if ($status == 301 or $status == 302 or $status == 303) { - # HTTP/1.1 is unclear on how to mutate the method - $method = "GET" unless $method eq "HEAD"; - $redirect = 1; - } elsif ($status == 307) { - $redirect = 1; - } + my $finish = sub { # ($data, $err_status, $err_reason[, $persistent]) + if ($state{handle}) { + # handle keepalive + if ( + $persistent + && $_[3] + && ($hdr{HTTPVersion} < 1.1 + ? $hdr{connection} =~ /\bkeep-?alive\b/i + : $hdr{connection} !~ /\bclose\b/i) + ) { + ka_store $ka_key, delete $state{handle}; + } else { + # no keepalive, destroy the handle + $state{handle}->destroy; } + } - my $finish = sub { # ($data, $err_status, $err_reason[, $keepalive]) - my $may_keep_alive = $_[3]; + %state = (); - $state{handle}->destroy if $state{handle}; - %state = (); + if (defined $_[1]) { + $hdr{OrigStatus} = $hdr{Status}; $hdr{Status} = $_[1]; + $hdr{OrigReason} = $hdr{Reason}; $hdr{Reason} = $_[2]; + } - if (defined $_[1]) { - $hdr{OrigStatus} = $hdr{Status}; $hdr{Status} = $_[1]; - $hdr{OrigReason} = $hdr{Reason}; $hdr{Reason} = $_[2]; - } + # set-cookie processing + if ($arg{cookie_jar}) { + cookie_jar_set_cookie $arg{cookie_jar}, $hdr{"set-cookie"}, $uhost, $hdr{date}; + } - # set-cookie processing - if ($arg{cookie_jar}) { - cookie_jar_set_cookie $arg{cookie_jar}, $hdr{"set-cookie"}, $uhost; - } + if ($redirect && exists $hdr{location}) { + # we ignore any errors, as it is very common to receive + # Content-Length != 0 but no actual body + # we also access %hdr, as $_[1] might be an erro + $state{recurse} = + http_request ( + $method => $hdr{location}, + %arg, + recurse => $recurse - 1, + Redirect => [$_[0], \%hdr], + sub { + %state = (); + &$cb + }, + ); + } else { + $cb->($_[0], \%hdr); + } + }; - if ($redirect && exists $hdr{location}) { - # we ignore any errors, as it is very common to receive - # Content-Length != 0 but no actual body - # we also access %hdr, as $_[1] might be an erro - http_request ( - $method => $hdr{location}, - %arg, - recurse => $recurse - 1, - Redirect => [$_[0], \%hdr], - $cb); - } else { - $cb->($_[0], \%hdr); - } - }; + $ae_error = 597; # body phase - my $len = $hdr{"content-length"}; + my $chunked = $hdr{"transfer-encoding"} =~ /\bchunked\b/i; # not quite correct... - if (!$redirect && $arg{on_header} && !$arg{on_header}(\%hdr)) { - $finish->(undef, 598 => "Request cancelled by on_header"); - } elsif ( - $hdr{Status} =~ /^(?:1..|204|205|304)$/ - or $method eq "HEAD" - or (defined $len && !$len) - ) { - # no body - $finish->("", undef, undef, 1); - } else { - # body handling, many different code paths - # - no body expected - # - want_body_handle - # - te chunked - # - 2x length known (with or without on_body) - # - 2x length not known (with or without on_body) - if (!$redirect && $arg{want_body_handle}) { - $_[0]->on_eof (undef); - $_[0]->on_error (undef); - $_[0]->on_read (undef); - - $finish->(delete $state{handle}); - - } elsif ($hdr{"transfer-encoding"} =~ /\bchunked\b/i) { - my $cl = 0; - my $body = undef; - my $on_body = $arg{on_body} || sub { $body .= shift; 1 }; - - $_[0]->on_error (sub { $finish->(undef, 599 => $_[2]) }); - - my $read_chunk; $read_chunk = sub { - $_[1] =~ /^([0-9a-fA-F]+)/ - or $finish->(undef, 599 => "Garbled chunked transfer encoding"); - - my $len = hex $1; - - if ($len) { - $cl += $len; - - $_[0]->push_read (chunk => $len, sub { - $on_body->($_[1], \%hdr) - or return $finish->(undef, 598 => "Request cancelled by on_body"); - - $_[0]->push_read (line => sub { - length $_[1] - and return $finish->(undef, 599 => "Garbled chunked transfer encoding"); - $_[0]->push_read (line => $read_chunk); - }); - }); - } else { - $hdr{"content-length"} ||= $cl; - - $_[0]->push_read (line => $qr_nlnl, sub { - if (length $_[1]) { - for ("$_[1]") { - y/\015//d; # weed out any \015, as they show up in the weirdest of places. - - my $hdr = parse_hdr - or return $finish->(undef, 599 => "Garbled response trailers"); - - %hdr = (%hdr, %$hdr); - } - } + my $len = $chunked ? undef : $hdr{"content-length"}; - $finish->($body, undef, undef, 1); - }); - } - }; + # body handling, many different code paths + # - no body expected + # - want_body_handle + # - te chunked + # - 2x length known (with or without on_body) + # - 2x length not known (with or without on_body) + if (!$redirect && $arg{on_header} && !$arg{on_header}(\%hdr)) { + $finish->(undef, 598 => "Request cancelled by on_header"); + } elsif ( + $hdr{Status} =~ /^(?:1..|204|205|304)$/ + or $method eq "HEAD" + or (defined $len && $len == 0) # == 0, not !, because "0 " is true + ) { + # no body + $finish->("", undef, undef, 1); + + } elsif (!$redirect && $arg{want_body_handle}) { + $_[0]->on_eof (undef); + $_[0]->on_error (undef); + $_[0]->on_read (undef); + + $finish->(delete $state{handle}); + + } elsif ($chunked) { + my $cl = 0; + my $body = ""; + my $on_body = (!$redirect && $arg{on_body}) || sub { $body .= shift; 1 }; + + $state{read_chunk} = sub { + $_[1] =~ /^([0-9a-fA-F]+)/ + or return $finish->(undef, $ae_error => "Garbled chunked transfer encoding"); + + my $len = hex $1; + + if ($len) { + $cl += $len; + + $_[0]->push_read (chunk => $len, sub { + $on_body->($_[1], \%hdr) + or return $finish->(undef, 598 => "Request cancelled by on_body"); + + $_[0]->push_read (line => sub { + length $_[1] + and return $finish->(undef, $ae_error => "Garbled chunked transfer encoding"); + $_[0]->push_read (line => $state{read_chunk}); + }); + }); + } else { + $hdr{"content-length"} ||= $cl; - $_[0]->push_read (line => $read_chunk); + $_[0]->push_read (line => $qr_nlnl, sub { + if (length $_[1]) { + for ("$_[1]") { + y/\015//d; # weed out any \015, as they show up in the weirdest of places. - } elsif ($arg{on_body}) { - $_[0]->on_error (sub { $finish->(undef, 599 => $_[2]) }); + my $hdr = _parse_hdr + or return $finish->(undef, $ae_error => "Garbled response trailers"); - if ($len) { - $_[0]->on_read (sub { - $len -= length $_[0]{rbuf}; - - $arg{on_body}(delete $_[0]{rbuf}, \%hdr) - or return $finish->(undef, 598 => "Request cancelled by on_body"); - - $len > 0 - or $finish->("", undef, undef, 1); - }); - } else { - $_[0]->on_eof (sub { - $finish->(""); - }); - $_[0]->on_read (sub { - $arg{on_body}(delete $_[0]{rbuf}, \%hdr) - or $finish->(undef, 598 => "Request cancelled by on_body"); - }); + %hdr = (%hdr, %$hdr); + } } - } else { - $_[0]->on_eof (undef); - if ($len) { - $_[0]->on_error (sub { $finish->(undef, 599 => $_[2]) }); - $_[0]->on_read (sub { - $finish->((substr delete $_[0]{rbuf}, 0, $len, ""), undef, undef, 1) - if $len <= length $_[0]{rbuf}; - }); - } else { - $_[0]->on_error (sub { - ($! == Errno::EPIPE || !$!) - ? $finish->(delete $_[0]{rbuf}) - : $finish->(undef, 599 => $_[2]); - }); - $_[0]->on_read (sub { }); - } - } + $finish->($body, undef, undef, 1); + }); } }; - $state{handle}->push_read (line => $qr_nlnl, $state{read_response}); - }; + $_[0]->push_read (line => $state{read_chunk}); - # now handle proxy-CONNECT method - if ($proxy && $uscheme eq "https") { - # oh dear, we have to wrap it into a connect request - - # maybe re-use $uauthority with patched port? - $state{handle}->push_write ("CONNECT $uhost:$uport HTTP/1.0\015\012Host: $uhost\015\012\015\012"); - $state{handle}->push_read (line => $qr_nlnl, sub { - $_[1] =~ /^HTTP\/([0-9\.]+) \s+ ([0-9]{3}) (?: \s+ ([^\015\012]*) )?/ix - or return (%state = (), $cb->(undef, { @pseudo, Status => 599, Reason => "Invalid proxy connect response ($_[1])" })); - - if ($2 == 200) { - $rpath = $upath; - &$handle_actual_request; - } else { - %state = (); - $cb->(undef, { @pseudo, Status => $2, Reason => $3 }); - } - }); + } elsif (!$redirect && $arg{on_body}) { + if (defined $len) { + $_[0]->on_read (sub { + $len -= length $_[0]{rbuf}; + + $arg{on_body}(delete $_[0]{rbuf}, \%hdr) + or return $finish->(undef, 598 => "Request cancelled by on_body"); + + $len > 0 + or $finish->("", undef, undef, 1); + }); + } else { + $_[0]->on_eof (sub { + $finish->(""); + }); + $_[0]->on_read (sub { + $arg{on_body}(delete $_[0]{rbuf}, \%hdr) + or $finish->(undef, 598 => "Request cancelled by on_body"); + }); + } } else { - &$handle_actual_request; + $_[0]->on_eof (undef); + + if (defined $len) { + $_[0]->on_read (sub { + $finish->((substr delete $_[0]{rbuf}, 0, $len, ""), undef, undef, 1) + if $len <= length $_[0]{rbuf}; + }); + } else { + $_[0]->on_error (sub { + ($! == Errno::EPIPE || !$!) + ? $finish->(delete $_[0]{rbuf}) + : $finish->(undef, $ae_error => $_[2]); + }); + $_[0]->on_read (sub { }); + } } }; - my $tcp_connect = $arg{tcp_connect} - || do { require AnyEvent::Socket; \&AnyEvent::Socket::tcp_connect }; + # if keepalive is enabled, then the server closing the connection + # before a response can happen legally - we retry on idempotent methods. + if ($was_persistent && $idempotent) { + my $old_eof = $hdl->{on_eof}; + $hdl->{on_eof} = sub { + _destroy_state %state; + + %state = (); + $state{recurse} = + http_request ( + $method => $url, + %arg, + recurse => $recurse - 1, + persistent => 0, + sub { + %state = (); + &$cb + } + ); + }; + $hdl->on_read (sub { + return unless %state; + + # as soon as we receive something, a connection close + # once more becomes a hard error + $hdl->{on_eof} = $old_eof; + $hdl->push_read (line => $qr_nlnl, $state{read_response}); + }); + } else { + $hdl->push_read (line => $qr_nlnl, $state{read_response}); + } + }; + + my $prepare_handle = sub { + my ($hdl) = $state{handle}; + + $hdl->on_error (sub { + _error %state, $cb, { @pseudo, Status => $ae_error, Reason => $_[2] }; + }); + $hdl->on_eof (sub { + _error %state, $cb, { @pseudo, Status => $ae_error, Reason => "Unexpected end-of-file" }; + }); + $hdl->timeout_reset; + $hdl->timeout ($timeout); + }; + + # connected to proxy (or origin server) + my $connect_cb = sub { + my $fh = shift + or return _error %state, $cb, { @pseudo, Status => $ae_error, Reason => "$!" }; + + return unless delete $state{connect_guard}; + + # get handle + $state{handle} = new AnyEvent::Handle + %{ $arg{handle_params} }, + fh => $fh, + peername => $uhost, + tls_ctx => $arg{tls_ctx}, + ; + + $prepare_handle->(); + + #$state{handle}->starttls ("connect") if $rscheme eq "https"; + + # now handle proxy-CONNECT method + if ($proxy && $uscheme eq "https") { + # oh dear, we have to wrap it into a connect request + + my $auth = exists $hdr{"proxy-authorization"} + ? "proxy-authorization: " . (delete $hdr{"proxy-authorization"}) . "\015\012" + : ""; + + # maybe re-use $uauthority with patched port? + $state{handle}->push_write ("CONNECT $uhost:$uport HTTP/1.0\015\012$auth\015\012"); + $state{handle}->push_read (line => $qr_nlnl, sub { + $_[1] =~ /^HTTP\/([0-9\.]+) \s+ ([0-9]{3}) (?: \s+ ([^\015\012]*) )?/ix + or return _error %state, $cb, { @pseudo, Status => 599, Reason => "Invalid proxy connect response ($_[1])" }; + + if ($2 == 200) { + $rpath = $upath; + $handle_actual_request->(); + } else { + _error %state, $cb, { @pseudo, Status => $2, Reason => $3 }; + } + }); + } else { + delete $hdr{"proxy-authorization"} unless $proxy; + + $handle_actual_request->(); + } + }; + + _get_slot $uhost, sub { + $state{slot_guard} = shift; + + return unless $state{connect_guard}; + + # try to use an existing keepalive connection, but only if we, ourselves, plan + # on a keepalive request (in theory, this should be a separate config option). + if ($persistent && $KA_CACHE{$ka_key}) { + $was_persistent = 1; + + $state{handle} = ka_fetch $ka_key; +# $state{handle}->destroyed +# and die "AnyEvent::HTTP: unexpectedly got a destructed handle (1), please report.";#d# + $prepare_handle->(); +# $state{handle}->destroyed +# and die "AnyEvent::HTTP: unexpectedly got a destructed handle (2), please report.";#d# + $rpath = $upath; + $handle_actual_request->(); - $state{connect_guard} = $tcp_connect->($rhost, $rport, $connect_cb, $arg{on_prepare} || sub { $timeout }); + } else { + my $tcp_connect = $arg{tcp_connect} + || do { require AnyEvent::Socket; \&AnyEvent::Socket::tcp_connect }; + $state{connect_guard} = $tcp_connect->($rhost, $rport, $connect_cb, $arg{on_prepare} || sub { $timeout }); + } }; - defined wantarray && AnyEvent::Util::guard { %state = () } + defined wantarray && AnyEvent::Util::guard { _destroy_state %state } } sub http_get($@) { @@ -929,7 +1281,7 @@ hostnames. The latter is a simple stub resolver and does no caching on its own. If you want DNS caching, you currently have to provide your own default resolver (by storing a suitable resolver object in -C<$AnyEvent::DNS::RESOLVER>). +C<$AnyEvent::DNS::RESOLVER>) or your own C callback. =head2 GLOBAL FUNCTIONS AND VARIABLES @@ -938,11 +1290,54 @@ =item AnyEvent::HTTP::set_proxy "proxy-url" Sets the default proxy server to use. The proxy-url must begin with a -string of the form C (optionally C), croaks -otherwise. +string of the form C, croaks otherwise. To clear an already-set proxy, use C. +When AnyEvent::HTTP is loaded for the first time it will query the +default proxy from the operating system, currently by looking at +C<$ENV{http_proxy>}. + +=item AnyEvent::HTTP::cookie_jar_expire $jar[, $session_end] + +Remove all cookies from the cookie jar that have been expired. If +C<$session_end> is given and true, then additionally remove all session +cookies. + +You should call this function (with a true C<$session_end>) before you +save cookies to disk, and you should call this function after loading them +again. If you have a long-running program you can additionally call this +function from time to time. + +A cookie jar is initially an empty hash-reference that is managed by this +module. Its format is subject to change, but currently it is as follows: + +The key C has to contain C<2>, otherwise the hash gets +cleared. All other keys are hostnames or IP addresses pointing to +hash-references. The key for these inner hash references is the +server path for which this cookie is meant, and the values are again +hash-references. Each key of those hash-references is a cookie name, and +the value, you guessed it, is another hash-reference, this time with the +key-value pairs from the cookie, except for C and C, +which have been replaced by a C<_expires> key that contains the cookie +expiry timestamp. Session cookies are indicated by not having an +C<_expires> key. + +Here is an example of a cookie jar with a single cookie, so you have a +chance of understanding the above paragraph: + + { + version => 2, + "10.0.0.1" => { + "/" => { + "mythweb_id" => { + _expires => 1293917923, + value => "ooRung9dThee3ooyXooM1Ohm", + }, + }, + }, + } + =item $date = AnyEvent::HTTP::format_date $timestamp Takes a POSIX timestamp (seconds since the epoch) and formats it as a HTTP @@ -950,14 +1345,18 @@ =item $timestamp = AnyEvent::HTTP::parse_date $date -Takes a HTTP Date (RFC 2616) or a Cookie date (netscape cookie spec) and -returns the corresponding POSIX timestamp, or C if the date cannot -be parsed. +Takes a HTTP Date (RFC 2616) or a Cookie date (netscape cookie spec) or a +bunch of minor variations of those, and returns the corresponding POSIX +timestamp, or C if the date cannot be parsed. =item $AnyEvent::HTTP::MAX_RECURSE The default value for the C request parameter (default: C<10>). +=item $AnyEvent::HTTP::TIMEOUT + +The default timeout for connection operations (default: C<300>). + =item $AnyEvent::HTTP::USERAGENT The default value for the C header (the default is @@ -966,17 +1365,28 @@ =item $AnyEvent::HTTP::MAX_PER_HOST The maximum number of concurrent connections to the same host (identified -by the hostname). If the limit is exceeded, then the additional requests -are queued until previous connections are closed. +by the hostname). If the limit is exceeded, then additional requests +are queued until previous connections are closed. Both persistent and +non-persistent connections are counted in this limit. The default value for this is C<4>, and it is highly advisable to not -increase it. +increase it much. + +For comparison: the RFC's recommend 4 non-persistent or 2 persistent +connections, older browsers used 2, newer ones (such as firefox 3) +typically use 6, and Opera uses 8 because like, they have the fastest +browser and give a shit for everybody else on the planet. + +=item $AnyEvent::HTTP::PERSISTENT_TIMEOUT + +The time after which idle persistent connections get closed by +AnyEvent::HTTP (default: C<3>). =item $AnyEvent::HTTP::ACTIVE The number of active connections. This is not the number of currently running requests, but the number of currently open and non-idle TCP -connections. This number of can be useful for load-leveling. +connections. This number can be useful for load-leveling. =back @@ -1001,17 +1411,17 @@ my ($d, $m, $y, $H, $M, $S); - if ($date =~ /^[A-Z][a-z][a-z], ([0-9][0-9])[\- ]([A-Z][a-z][a-z])[\- ]([0-9][0-9][0-9][0-9]) ([0-9][0-9]):([0-9][0-9]):([0-9][0-9]) GMT$/) { + if ($date =~ /^[A-Z][a-z][a-z]+, ([0-9][0-9]?)[\- ]([A-Z][a-z][a-z])[\- ]([0-9][0-9][0-9][0-9]) ([0-9][0-9]?):([0-9][0-9]?):([0-9][0-9]?) GMT$/) { # RFC 822/1123, required by RFC 2616 (with " ") # cookie dates (with "-") ($d, $m, $y, $H, $M, $S) = ($1, $2, $3, $4, $5, $6); - } elsif ($date =~ /^[A-Z][a-z]+, ([0-9][0-9])-([A-Z][a-z][a-z])-([0-9][0-9]) ([0-9][0-9]):([0-9][0-9]):([0-9][0-9]) GMT$/) { + } elsif ($date =~ /^[A-Z][a-z][a-z]+, ([0-9][0-9]?)-([A-Z][a-z][a-z])-([0-9][0-9]) ([0-9][0-9]?):([0-9][0-9]?):([0-9][0-9]?) GMT$/) { # RFC 850 ($d, $m, $y, $H, $M, $S) = ($1, $2, $3 < 69 ? $3 + 2000 : $3 + 1900, $4, $5, $6); - } elsif ($date =~ /^[A-Z][a-z][a-z] ([A-Z][a-z][a-z]) ([0-9 ][0-9]) ([0-9][0-9]):([0-9][0-9]):([0-9][0-9]) ([0-9][0-9][0-9][0-9])$/) { + } elsif ($date =~ /^[A-Z][a-z][a-z]+ ([A-Z][a-z][a-z]) ([0-9 ]?[0-9]) ([0-9][0-9]?):([0-9][0-9]?):([0-9][0-9]?) ([0-9][0-9][0-9][0-9])$/) { # ISO C's asctime ($d, $m, $y, $H, $M, $S) = ($2, $1, $6, $3, $4, $5); } @@ -1020,7 +1430,7 @@ for (0..11) { if ($m eq $month[$_]) { require Time::Local; - return Time::Local::timegm ($S, $M, $H, $d, $_, $y); + return eval { Time::Local::timegm ($S, $M, $H, $d, $_, $y) }; } } @@ -1029,7 +1439,7 @@ sub set_proxy($) { if (length $_[0]) { - $_[0] =~ m%^(https?):// ([^:/]+) (?: : (\d*) )?%ix + $_[0] =~ m%^(http):// ([^:/]+) (?: : (\d*) )?%ix or Carp::croak "$_[0]: invalid proxy URL"; $PROXY = [$2, $3 || 3128, $1] } else { @@ -1042,7 +1452,107 @@ set_proxy $ENV{http_proxy}; }; -=head2 SOCKS PROXIES +=head2 SHOWCASE + +This section contains some more elaborate "real-world" examples or code +snippets. + +=head2 HTTP/1.1 FILE DOWNLOAD + +Downloading files with HTTP can be quite tricky, especially when something +goes wrong and you want to resume. + +Here is a function that initiates and resumes a download. It uses the +last modified time to check for file content changes, and works with many +HTTP/1.0 servers as well, and usually falls back to a complete re-download +on older servers. + +It calls the completion callback with either C, which means a +nonretryable error occurred, C<0> when the download was partial and should +be retried, and C<1> if it was successful. + + use AnyEvent::HTTP; + + sub download($$$) { + my ($url, $file, $cb) = @_; + + open my $fh, "+<", $file + or die "$file: $!"; + + my %hdr; + my $ofs = 0; + + if (stat $fh and -s _) { + $ofs = -s _; + warn "-s is ", $ofs; + $hdr{"if-unmodified-since"} = AnyEvent::HTTP::format_date +(stat _)[9]; + $hdr{"range"} = "bytes=$ofs-"; + } + + http_get $url, + headers => \%hdr, + on_header => sub { + my ($hdr) = @_; + + if ($hdr->{Status} == 200 && $ofs) { + # resume failed + truncate $fh, $ofs = 0; + } + + sysseek $fh, $ofs, 0; + + 1 + }, + on_body => sub { + my ($data, $hdr) = @_; + + if ($hdr->{Status} =~ /^2/) { + length $data == syswrite $fh, $data + or return; # abort on write errors + } + + 1 + }, + sub { + my (undef, $hdr) = @_; + + my $status = $hdr->{Status}; + + if (my $time = AnyEvent::HTTP::parse_date $hdr->{"last-modified"}) { + utime $time, $time, $fh; + } + + if ($status == 200 || $status == 206 || $status == 416) { + # download ok || resume ok || file already fully downloaded + $cb->(1, $hdr); + + } elsif ($status == 412) { + # file has changed while resuming, delete and retry + unlink $file; + $cb->(0, $hdr); + + } elsif ($status == 500 or $status == 503 or $status =~ /^59/) { + # retry later + $cb->(0, $hdr); + + } else { + $cb->(undef, $hdr); + } + } + ; + } + + download "http://server/somelargefile", "/tmp/somelargefile", sub { + if ($_[0]) { + print "OK!\n"; + } elsif (defined $_[0]) { + print "please retry later\n"; + } else { + print "ERROR\n"; + } + }; + +=head3 SOCKS PROXIES Socks proxies are not directly supported by AnyEvent::HTTP. You can compile your perl to support socks, or use an external program such as