File Coverage

blib/lib/LWP/Protocol/Net/Curl.pm
Criterion Covered Total %
statement 28 30 93.3
branch n/a
condition n/a
subroutine 10 10 100.0
pod n/a
total 38 40 95.0


line stmt bran cond sub pod time code
1             package LWP::Protocol::Net::Curl;
2             # ABSTRACT: the power of libcurl in the palm of your hands!
3              
4              
5 7     7   57749 use strict;
  7         11  
  7         219  
6 7     7   25 use utf8;
  7         8  
  7         30  
7 7     7   128 use warnings qw(all);
  7         7  
  7         232  
8              
9 7     7   26 use base qw(LWP::Protocol);
  7         7  
  7         3829  
10              
11 7     7   175446 use Carp qw(carp);
  7         14  
  7         422  
12 7     7   33 use Config;
  7         11  
  7         250  
13 7     7   27 use Fcntl;
  7         9  
  7         1512  
14 7     7   3929 use HTTP::Date;
  7         20576  
  7         442  
15 7     7   457727 use LWP::UserAgent;
  7         48929  
  7         226  
16 7     7   6835 use Net::Curl::Easy qw(:constants);
  0            
  0            
17             use Net::Curl::Multi qw(:constants);
18             use Net::Curl::Share qw(:constants);
19             use Scalar::Util qw(looks_like_number);
20             use URI;
21              
22             our $VERSION = '0.023'; # VERSION
23              
24             my %curlopt;
25             my $share;
26             unless (defined $Config{usethreads}) {
27             $share = Net::Curl::Share->new({ started => time });
28             $share->setopt(CURLSHOPT_SHARE ,=> CURL_LOCK_DATA_COOKIE);
29             $share->setopt(CURLSHOPT_SHARE ,=> CURL_LOCK_DATA_DNS);
30              
31             ## no critic (RequireCheckingReturnValueOfEval)
32             eval { $share->setopt(CURLSHOPT_SHARE ,=> CURL_LOCK_DATA_SSL_SESSION) };
33             }
34              
35             ## no critic (ProhibitPackageVars)
36             my %protocols = map { ($_) x 2 } @{Net::Curl::version_info()->{protocols}};
37             our @implements =
38             sort grep { defined }
39             @protocols
40             {qw{ftp ftps gopher http https sftp scp}};
41             our %implements = map { $_ => 1 } @implements;
42              
43             our $use_select = Net::Curl::Multi->can(q(wait)) ? 0 : 1;
44              
45              
46             # Resolve libcurl constants by string
47             sub _curlopt {
48             my ($key, $no_carp) = @_;
49             return 0 + $key if looks_like_number($key);
50              
51             $key =~ s/^Net::Curl::Easy:://ix;
52             $key =~ y/-/_/;
53             $key =~ s/\W//gx;
54             $key = uc $key;
55             $key = qq(CURLOPT_${key}) if $key !~ /^CURL(?:M|SH)?OPT_/x;
56              
57             my $const = eval {
58             ## no critic (ProhibitNoStrict ProhibitNoWarnings)
59             no strict qw(refs);
60             no warnings qw(once);
61             return *$key->();
62             };
63             carp qq(Invalid libcurl constant: $key)
64             if $@
65             and not defined $no_carp;
66              
67             return $const;
68             }
69              
70             # Sugar for a common setopt() pattern
71             sub _setopt_ifdef {
72             my ($curl, $key, $value, $no_carp) = @_;
73              
74             my $curlopt_key = _curlopt($key, $no_carp);
75             $curl->setopt($curlopt_key => $value)
76             if defined $curlopt_key
77             and defined $value;
78              
79             return;
80             }
81              
82             # Pre-configure the module
83             sub import {
84             my ($class, @args) = @_;
85              
86             my $takeover = 1;
87             if (@args) {
88             my %args = @args;
89             while (my ($key, $value) = each %args) {
90             if ($key eq q(takeover)) {
91             $takeover = $value;
92             } else {
93             my $const = _curlopt($key);
94             $curlopt{$const} = $value
95             if defined $const;
96             }
97             }
98             }
99              
100             if ($takeover) {
101             LWP::Protocol::implementor($_ => $class)
102             for @implements;
103             }
104              
105             return;
106             }
107              
108             # Properly setup libcurl to handle each method in a compatible way
109             sub _handle_method {
110             my ($ua, $easy, $request) = @_;
111              
112             my $method = uc $request->method;
113             my %dispatch = (
114             GET => sub {
115             $easy->setopt(CURLOPT_HTTPGET ,=> 1);
116             }, POST => sub {
117             $easy->setopt(CURLOPT_POST ,=> 1);
118             $easy->setopt(CURLOPT_POSTFIELDS,=> $request->content);
119             $easy->setopt(CURLOPT_POSTFIELDSIZE,=> length $request->content);
120             }, HEAD => sub {
121             $easy->setopt(CURLOPT_NOBODY ,=> 1);
122             }, DELETE => sub {
123             $easy->setopt(CURLOPT_CUSTOMREQUEST ,=> $method);
124             }, PUT => sub {
125             $easy->setopt(CURLOPT_UPLOAD ,=> 1);
126             my $buf = $request->content;
127             my $off = 0;
128             # Do not set CURLOPT_INFILESIZE if Content-Length header exists
129             # and libcurl version is earlier than 7.23.0 (note libcurl will
130             # send two Content-Length headers in versions earlier than 7.23.0
131             # when both the Content-Length header and CURLOPT_INFILESIZE
132             # option is set).
133             $easy->setopt(CURLOPT_INFILESIZE,=> length $buf)
134             if !defined $request->header('Content-Length')
135             || Net::Curl::version_info()->{version_num} >= 0x72300;
136             $easy->setopt(CURLOPT_READFUNCTION ,=> sub {
137             my (undef, $maxlen) = @_;
138             my $chunk = substr $buf, $off, $maxlen;
139             $off += length $chunk;
140             return \$chunk;
141             });
142             },
143             );
144              
145             my $method_ref = $dispatch{$method};
146             if (defined $method_ref) {
147             $method_ref->();
148             } else {
149             ## no critic (RequireCarping)
150             die HTTP::Response->new(
151             &HTTP::Status::RC_BAD_REQUEST,
152             qq(Bad method '$method')
153             );
154             }
155              
156             # handle redirects internally (except POST, greatly fsck'd up by IIS servers)
157             if ($method ne q(POST) and grep { $method eq uc } @{$ua->requests_redirectable}) {
158             $easy->setopt(CURLOPT_AUTOREFERER ,=> 1);
159             $easy->setopt(CURLOPT_FOLLOWLOCATION,=> 1);
160             $easy->setopt(CURLOPT_MAXREDIRS ,=> $ua->max_redirect);
161             } else {
162             $easy->setopt(CURLOPT_FOLLOWLOCATION,=> 0);
163             }
164              
165             return $method;
166             }
167              
168             # Compatibilize request headers
169             sub _fix_headers {
170             my ($ua, $easy, $key, $value) = @_;
171              
172             return 0 unless defined $value;
173              
174             # stolen from LWP::Protocol::http
175             $key =~ s/^://x;
176             $value =~ s/\n/ /gx;
177              
178             my $encoding = 0;
179             if ($key =~ /^accept-encoding$/ix) {
180             my @encoding =
181             map { /^(?:x-)?(deflate|gzip|identity)$/ix ? lc $1 : () }
182             split /\s*,\s*/x, $value;
183              
184             if (@encoding) {
185             ++$encoding;
186             $easy->setopt(CURLOPT_ENCODING ,=> join(q(,) => @encoding));
187             }
188             } elsif ($key =~ /^user-agent$/ix) {
189             # While we try our best to look like LWP on the client-side,
190             # it's *definitely* different on the server-site!
191             # I guess it would be nice to introduce ourselves in a polite way.
192             $value =~ s/\b(\Q@{[ $ua->_agent ]}\E)\b/qq($1 ) . Net::Curl::version()/egx;
193             $easy->setopt(CURLOPT_USERAGENT ,=> $value);
194             } elsif ($key =~ /^x[-_](curlopt[-\w]+)$/ix) {
195             _setopt_ifdef($easy, $1 => $value);
196             } else {
197             $easy->pushopt(CURLOPT_HTTPHEADER ,=> [qq[$key: $value]]);
198             }
199              
200             return $encoding;
201             }
202              
203             # Wrap libcurl perform() in a (potentially) non-blocking way
204             sub _perform_loop {
205             my ($multi) = @_;
206              
207             my $running = 0;
208             do {
209             my $timeout = $multi->timeout;
210              
211             if ($running and $timeout > 9) {
212             if ($use_select) {
213             my ($r, $w, $e) = $multi->fdset;
214             select($r, $w, $e, $timeout / 1000);
215             } else {
216             $multi->wait($timeout);
217             }
218             }
219              
220             $running = $multi->perform;
221             while (my (undef, $easy, $result) = $multi->info_read) {
222             $multi->remove_handle($easy);
223             if ($result == CURLE_TOO_MANY_REDIRECTS) {
224             # will return the last request
225             } elsif ($result) {
226             ## no critic (RequireCarping)
227             die HTTP::Response->new(
228             &HTTP::Status::RC_BAD_REQUEST,
229             qq($result),
230             );
231             }
232             }
233             } while ($running);
234              
235             return $running;
236             }
237              
238             ## no critic (ProhibitManyArgs)
239             sub request {
240             my ($self, $request, $proxy, $arg, $size, $timeout) = @_;
241              
242             my $ua = $self->{ua};
243             unless (q(Net::Curl::Multi) eq ref $ua->{curl_multi}) {
244             $ua->{curl_multi} = Net::Curl::Multi->new({ def_headers => $ua->{def_headers} });
245              
246             # avoid "callback function is not set" warning
247             _setopt_ifdef(
248             $ua->{curl_multi},
249             q(CURLMOPT_SOCKETFUNCTION) => sub { return 0 },
250             1,
251             );
252             }
253              
254             my $data = '';
255             my $header = '';
256             my $writedata;
257              
258             my $easy = Net::Curl::Easy->new({ request => $request });
259             $ua->{curl_multi}->add_handle($easy);
260              
261             my $previous = undef;
262             my $response = HTTP::Response->new(&HTTP::Status::RC_OK);
263             $response->request($request);
264              
265             $easy->setopt(CURLOPT_HEADERFUNCTION ,=> sub {
266             my ($_easy, $line) = @_;
267             $header .= $line;
268              
269             # I hope only HTTP sends "empty line" as delimiters
270             if ($line =~ /^\s*$/sx) {
271             $response = HTTP::Response->parse($header);
272             my $msg = $response->message;
273             $msg = '' unless defined $msg;
274             $msg =~ s/^\s+|\s+$//gsx;
275             $response->message($msg);
276              
277             $response->request($request->clone);
278             my $effective_url = URI->new('' . $_easy->getinfo(CURLINFO_EFFECTIVE_URL));
279             $response->request->uri($effective_url);
280             $response->previous($previous) if defined $previous;
281             $previous = $response;
282              
283             $header = '';
284             }
285              
286             return length $line;
287             });
288              
289             if (q(CODE) eq ref $arg) {
290             $easy->setopt(CURLOPT_WRITEFUNCTION ,=> sub {
291             my (undef, $chunk) = @_;
292             $arg->($chunk, $response, $self);
293             return length $chunk;
294             });
295             $writedata = undef;
296             } elsif (defined $arg) {
297             # will die() later
298             sysopen $writedata, $arg, O_CREAT | O_NONBLOCK | O_WRONLY;
299             binmode $writedata;
300             } else {
301             $writedata = \$data;
302             }
303              
304             my $encoding = 0;
305             while (my ($key, $value) = each %curlopt) {
306             ++$encoding if $key == CURLOPT_ENCODING;
307             $easy->setopt($key, $value);
308             }
309              
310             # SSL stuff, may not be compiled
311             if ($request->uri->scheme =~ /s$/ix) {
312             _setopt_ifdef($easy, CAINFO => $ua->{ssl_opts}{SSL_ca_file});
313             _setopt_ifdef($easy, CAPATH => $ua->{ssl_opts}{SSL_ca_path});
314             _setopt_ifdef($easy, CURLOPT_SSLCERT=> $ua->{ssl_opts}{SSL_cert_file});
315             _setopt_ifdef($easy, CURLOPT_SSLKEY => $ua->{ssl_opts}{SSL_key_file});
316              
317             # fixes a security flaw denied by libcurl v7.28.1
318             _setopt_ifdef($easy, SSL_VERIFYHOST => (!!$ua->{ssl_opts}{verify_hostname}) << 1);
319             _setopt_ifdef($easy, SSL_VERIFYPEER => 0) unless $ua->{ssl_opts}{verify_hostname};
320             }
321              
322             $easy->setopt(CURLOPT_FILETIME ,=> 1);
323             $easy->setopt(CURLOPT_URL ,=> $request->uri);
324             _setopt_ifdef($easy, CURLOPT_BUFFERSIZE ,=> $size);
325             _setopt_ifdef($easy, CURLOPT_INTERFACE ,=> $ua->{local_address});
326             _setopt_ifdef($easy, CURLOPT_MAXFILESIZE,=> $ua->max_size);
327             _setopt_ifdef($easy, q(CURLOPT_NOPROXY) => join(q(,) => @{$ua->{no_proxy}}), 1);
328             _setopt_ifdef($easy, CURLOPT_PROXY ,=> $proxy);
329             _setopt_ifdef($easy, CURLOPT_SHARE ,=> $share);
330             _setopt_ifdef($easy, CURLOPT_TIMEOUT ,=> $timeout);
331             _setopt_ifdef($easy, CURLOPT_WRITEDATA ,=> $writedata);
332              
333             if ($ua->{show_progress}) {
334             $easy->setopt(CURLOPT_NOPROGRESS ,=> 0);
335             _setopt_ifdef(
336             $easy,
337             q(CURLOPT_PROGRESSFUNCTION) => sub {
338             my (undef, $dltotal, $dlnow) = @_;
339             $ua->progress($dltotal ? $dlnow / $dltotal : q(tick));
340             return 0;
341             },
342             1,
343             );
344             }
345              
346             _handle_method($ua, $easy, $request);
347              
348             $request->headers->scan(sub { $encoding += _fix_headers($ua, $easy, @_) });
349              
350             _perform_loop($ua->{curl_multi});
351              
352             $response->code($easy->getinfo(CURLINFO_RESPONSE_CODE) || 200);
353              
354             my $time = $easy->getinfo(CURLINFO_FILETIME);
355             $response->headers->header(last_modified => time2str($time))
356             if $time > 0;
357              
358             # handle decoded_content() & direct file write
359             if (q(GLOB) eq ref $writedata) {
360             close $writedata;
361             # avoid truncate by collect()
362             $arg = undef;
363             } elsif ($encoding) {
364             $response->headers->header(content_encoding => q(identity));
365             }
366              
367             return $self->collect_once($arg, $response, $data);
368             }
369              
370              
371             1;
372              
373             __END__