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   93139 use strict;
  7         14  
  7         320  
6 7     7   37 use utf8;
  7         14  
  7         42  
7 7     7   176 use warnings qw(all);
  7         11  
  7         299  
8              
9 7     7   36 use base qw(LWP::Protocol);
  7         11  
  7         6946  
10              
11 7     7   308020 use Carp qw(carp);
  7         17  
  7         496  
12 7     7   59 use Config;
  7         14  
  7         288  
13 7     7   41 use Fcntl;
  7         10  
  7         2504  
14 7     7   9537 use HTTP::Date;
  7         32175  
  7         499  
15 7     7   7704 use LWP::UserAgent;
  7         75645  
  7         444  
16 7     7   11672 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.022'; # 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             $easy->setopt(CURLOPT_INFILESIZE,=> length $buf);
129             $easy->setopt(CURLOPT_READFUNCTION ,=> sub {
130             my (undef, $maxlen) = @_;
131             my $chunk = substr $buf, $off, $maxlen;
132             $off += length $chunk;
133             return \$chunk;
134             });
135             },
136             );
137              
138             my $method_ref = $dispatch{$method};
139             if (defined $method_ref) {
140             $method_ref->();
141             } else {
142             ## no critic (RequireCarping)
143             die HTTP::Response->new(
144             &HTTP::Status::RC_BAD_REQUEST,
145             qq(Bad method '$method')
146             );
147             }
148              
149             # handle redirects internally (except POST, greatly fsck'd up by IIS servers)
150             if ($method ne q(POST) and grep { $method eq uc } @{$ua->requests_redirectable}) {
151             $easy->setopt(CURLOPT_AUTOREFERER ,=> 1);
152             $easy->setopt(CURLOPT_FOLLOWLOCATION,=> 1);
153             $easy->setopt(CURLOPT_MAXREDIRS ,=> $ua->max_redirect);
154             } else {
155             $easy->setopt(CURLOPT_FOLLOWLOCATION,=> 0);
156             }
157              
158             return $method;
159             }
160              
161             # Compatibilize request headers
162             sub _fix_headers {
163             my ($ua, $easy, $key, $value) = @_;
164              
165             return 0 unless defined $value;
166              
167             # stolen from LWP::Protocol::http
168             $key =~ s/^://x;
169             $value =~ s/\n/ /gx;
170              
171             my $encoding = 0;
172             if ($key =~ /^accept-encoding$/ix) {
173             my @encoding =
174             map { /^(?:x-)?(deflate|gzip|identity)$/ix ? lc $1 : () }
175             split /\s*,\s*/x, $value;
176              
177             if (@encoding) {
178             ++$encoding;
179             $easy->setopt(CURLOPT_ENCODING ,=> join(q(,) => @encoding));
180             }
181             } elsif ($key =~ /^user-agent$/ix) {
182             # While we try our best to look like LWP on the client-side,
183             # it's *definitely* different on the server-site!
184             # I guess it would be nice to introduce ourselves in a polite way.
185             $value =~ s/\b(\Q@{[ $ua->_agent ]}\E)\b/qq($1 ) . Net::Curl::version()/egx;
186             $easy->setopt(CURLOPT_USERAGENT ,=> $value);
187             } elsif ($key =~ /^x[-_](curlopt[-\w]+)$/ix) {
188             _setopt_ifdef($easy, $1 => $value);
189             } else {
190             $easy->pushopt(CURLOPT_HTTPHEADER ,=> [qq[$key: $value]]);
191             }
192              
193             return $encoding;
194             }
195              
196             # Wrap libcurl perform() in a (potentially) non-blocking way
197             sub _perform_loop {
198             my ($multi) = @_;
199              
200             my $running = 0;
201             do {
202             my $timeout = $multi->timeout;
203              
204             if ($running and $timeout > 9) {
205             if ($use_select) {
206             my ($r, $w, $e) = $multi->fdset;
207             select($r, $w, $e, $timeout / 1000);
208             } else {
209             $multi->wait($timeout);
210             }
211             }
212              
213             $running = $multi->perform;
214             while (my (undef, $easy, $result) = $multi->info_read) {
215             $multi->remove_handle($easy);
216             if ($result == CURLE_TOO_MANY_REDIRECTS) {
217             # will return the last request
218             } elsif ($result) {
219             ## no critic (RequireCarping)
220             die HTTP::Response->new(
221             &HTTP::Status::RC_BAD_REQUEST,
222             qq($result),
223             );
224             }
225             }
226             } while ($running);
227              
228             return $running;
229             }
230              
231             ## no critic (ProhibitManyArgs)
232             sub request {
233             my ($self, $request, $proxy, $arg, $size, $timeout) = @_;
234              
235             my $ua = $self->{ua};
236             unless (q(Net::Curl::Multi) eq ref $ua->{curl_multi}) {
237             $ua->{curl_multi} = Net::Curl::Multi->new({ def_headers => $ua->{def_headers} });
238              
239             # avoid "callback function is not set" warning
240             _setopt_ifdef(
241             $ua->{curl_multi},
242             q(CURLMOPT_SOCKETFUNCTION) => sub { return 0 },
243             1,
244             );
245             }
246              
247             my $data = '';
248             my $header = '';
249             my $writedata;
250              
251             my $easy = Net::Curl::Easy->new({ request => $request });
252             $ua->{curl_multi}->add_handle($easy);
253              
254             my $previous = undef;
255             my $response = HTTP::Response->new(&HTTP::Status::RC_OK);
256             $response->request($request);
257              
258             $easy->setopt(CURLOPT_HEADERFUNCTION ,=> sub {
259             my ($_easy, $line) = @_;
260             $header .= $line;
261              
262             # I hope only HTTP sends "empty line" as delimiters
263             if ($line =~ /^\s*$/sx) {
264             $response = HTTP::Response->parse($header);
265             my $msg = $response->message;
266             $msg = '' unless defined $msg;
267             $msg =~ s/^\s+|\s+$//gsx;
268             $response->message($msg);
269              
270             $response->request($request->clone);
271             my $effective_url = URI->new('' . $_easy->getinfo(CURLINFO_EFFECTIVE_URL));
272             $response->request->uri($effective_url);
273             $response->previous($previous) if defined $previous;
274             $previous = $response;
275              
276             $header = '';
277             }
278              
279             return length $line;
280             });
281              
282             if (q(CODE) eq ref $arg) {
283             $easy->setopt(CURLOPT_WRITEFUNCTION ,=> sub {
284             my (undef, $chunk) = @_;
285             $arg->($chunk, $response, $self);
286             return length $chunk;
287             });
288             $writedata = undef;
289             } elsif (defined $arg) {
290             # will die() later
291             sysopen $writedata, $arg, O_CREAT | O_NONBLOCK | O_WRONLY;
292             binmode $writedata;
293             } else {
294             $writedata = \$data;
295             }
296              
297             my $encoding = 0;
298             while (my ($key, $value) = each %curlopt) {
299             ++$encoding if $key == CURLOPT_ENCODING;
300             $easy->setopt($key, $value);
301             }
302              
303             # SSL stuff, may not be compiled
304             if ($request->uri->scheme =~ /s$/ix) {
305             _setopt_ifdef($easy, CAINFO => $ua->{ssl_opts}{SSL_ca_file});
306             _setopt_ifdef($easy, CAPATH => $ua->{ssl_opts}{SSL_ca_path});
307             _setopt_ifdef($easy, CURLOPT_SSLCERT=> $ua->{ssl_opts}{SSL_cert_file});
308             _setopt_ifdef($easy, CURLOPT_SSLKEY => $ua->{ssl_opts}{SSL_key_file});
309              
310             # fixes a security flaw denied by libcurl v7.28.1
311             _setopt_ifdef($easy, SSL_VERIFYHOST => (!!$ua->{ssl_opts}{verify_hostname}) << 1);
312             _setopt_ifdef($easy, SSL_VERIFYPEER => 0) unless $ua->{ssl_opts}{verify_hostname};
313             }
314              
315             $easy->setopt(CURLOPT_FILETIME ,=> 1);
316             $easy->setopt(CURLOPT_URL ,=> $request->uri);
317             _setopt_ifdef($easy, CURLOPT_BUFFERSIZE ,=> $size);
318             _setopt_ifdef($easy, CURLOPT_INTERFACE ,=> $ua->{local_address});
319             _setopt_ifdef($easy, CURLOPT_MAXFILESIZE,=> $ua->max_size);
320             _setopt_ifdef($easy, q(CURLOPT_NOPROXY) => join(q(,) => @{$ua->{no_proxy}}), 1);
321             _setopt_ifdef($easy, CURLOPT_PROXY ,=> $proxy);
322             _setopt_ifdef($easy, CURLOPT_SHARE ,=> $share);
323             _setopt_ifdef($easy, CURLOPT_TIMEOUT ,=> $timeout);
324             _setopt_ifdef($easy, CURLOPT_WRITEDATA ,=> $writedata);
325              
326             if ($ua->{show_progress}) {
327             $easy->setopt(CURLOPT_NOPROGRESS ,=> 0);
328             _setopt_ifdef(
329             $easy,
330             q(CURLOPT_PROGRESSFUNCTION) => sub {
331             my (undef, $dltotal, $dlnow) = @_;
332             $ua->progress($dltotal ? $dlnow / $dltotal : q(tick));
333             return 0;
334             },
335             1,
336             );
337             }
338              
339             _handle_method($ua, $easy, $request);
340              
341             $request->headers->scan(sub { $encoding += _fix_headers($ua, $easy, @_) });
342              
343             _perform_loop($ua->{curl_multi});
344              
345             $response->code($easy->getinfo(CURLINFO_RESPONSE_CODE) || 200);
346              
347             my $time = $easy->getinfo(CURLINFO_FILETIME);
348             $response->headers->header(last_modified => time2str($time))
349             if $time > 0;
350              
351             # handle decoded_content() & direct file write
352             if (q(GLOB) eq ref $writedata) {
353             close $writedata;
354             # avoid truncate by collect()
355             $arg = undef;
356             } elsif ($encoding) {
357             $response->headers->header(content_encoding => q(identity));
358             }
359              
360             return $self->collect_once($arg, $response, $data);
361             }
362              
363              
364             1;
365              
366             __END__