File Coverage

blib/lib/WWW/Noss/Curl.pm
Criterion Covered Total %
statement 14 100 14.0
branch 0 44 0.0
condition 0 30 0.0
subroutine 5 9 55.5
pod 4 4 100.0
total 23 187 12.3


line stmt bran cond sub pod time code
1             package WWW::Noss::Curl;
2 1     1   22 use 5.016;
  1         5  
3 1     1   7 use strict;
  1         1  
  1         71  
4 1     1   6 use warnings;
  1         3  
  1         95  
5             our $VERSION = '2.02';
6              
7 1     1   7 use Exporter qw(import);
  1         2  
  1         66  
8             our @EXPORT_OK = qw(curl curl_error http_status_string parse_http_header);
9              
10 1     1   7 use File::Temp qw(tempfile);
  1         2  
  1         1662  
11              
12             # https://curl.se/docs/manpage.html
13             our %CODES = (
14             -1 => 'Failed to execute curl',
15             0 => 'Success',
16             1 => 'Unsupported protocol',
17             2 => 'Failed to initialize curl',
18             3 => 'Malformed URL',
19             4 => 'Required feature disabled',
20             5 => 'Could not resolve proxy',
21             6 => 'Could not resolve host',
22             7 => 'Failed to connect to host',
23             8 => 'Weird server reply',
24             9 => 'FTP access denied',
25             10 => 'FTP accept failed',
26             11 => 'Weird FTP PASS reply',
27             12 => 'FTP session timeout expired',
28             13 => 'Weird FTP PASV reply',
29             14 => 'Weird FTP 227 reply',
30             15 => 'Cannot use FTP host',
31             16 => 'HTTP/2 error',
32             17 => 'Failed to set FTP transfer to binary',
33             18 => 'Could only receive partial file',
34             19 => 'FTP file access error',
35             21 => 'FTP quote error',
36             22 => 'HTTP page not retrieved',
37             23 => 'Write error',
38             25 => 'FTP STOR error',
39             26 => 'Read error',
40             27 => 'Out of memory',
41             28 => 'Operation timeout',
42             30 => 'FTP PORT failed',
43             31 => 'FTP REST failed',
44             33 => 'HTTP range error',
45             34 => 'HTTP post error',
46             35 => 'SSL connection failed',
47             36 => 'Failed to resume download',
48             37 => 'Failed to open file',
49             38 => 'LDAP bind failed',
50             39 => 'LDAP search failed',
51             41 => 'LDAP function not found',
52             42 => 'Aborted',
53             43 => 'Internal error',
54             45 => 'Interface error',
55             47 => 'Too many redirects',
56             48 => 'Unknown option passed to libcurl',
57             49 => 'Mailformed telnet option',
58             52 => 'No server reply',
59             53 => 'SSL crypto engine not found',
60             54 => 'Cannot set SSL crypto engine as default',
61             55 => 'Failed sending network data',
62             56 => 'Failed to receive network data',
63             58 => 'Problem with local certificate',
64             59 => 'Could not use specified SSL cipher',
65             60 => 'Peer certificate cannot be authenticated with known CA certificates',
66             61 => 'Unrecognized transfer encoding',
67             63 => 'Maximum file size exceeded',
68             64 => 'Requested FTP SSL level failed',
69             65 => 'Rewind failed',
70             66 => 'Failed to initialize SSL engine',
71             67 => 'Username or password not accepted',
72             68 => 'TFTP file not found',
73             69 => 'TFTP permission error',
74             70 => 'TFTP out of disk space',
75             71 => 'Illegal TFTP operation',
76             72 => 'Unknown TFTP transfer ID',
77             73 => 'TFTP file already exists',
78             74 => 'TFTP no such user',
79             77 => 'Problem reading SSL CA cert',
80             78 => 'Resource referenced in URL does not exist',
81             79 => 'Unspecified error occurred during SSH session',
82             80 => 'Failed to shut down SSL connection',
83             82 => 'Could not load CRL file',
84             83 => 'Issuer check failed',
85             84 => 'FTP PRET failed',
86             85 => 'RTSP CSeq number mismatch',
87             86 => 'RTSP Session ID mismatch',
88             87 => 'Failed to parse FTP file list',
89             88 => 'FTP chunk callback error',
90             89 => 'No connection available',
91             90 => 'SSL public key does not match pinned public key',
92             91 => 'Invalid SSL certificate status',
93             92 => 'Stream error in HTTP/2 framing layer',
94             93 => 'API function called from inside callback',
95             94 => 'Authentication function returned error',
96             95 => 'Error detected in HTTP/3 layer',
97             96 => 'QUIC connection error',
98             97 => 'Proxy handshake error',
99             98 => 'Client-side certificate required to complete TLS handshake',
100             99 => 'Poll or select returned fatal error',
101             100 => 'Value or data field grew larger than allowed',
102             # and maybe some more in the future...
103             );
104              
105             # https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status
106             our %HTTP_CODES = (
107              
108             100 => 'Continue',
109             101 => 'Switching Protocols',
110             102 => 'Processing',
111             103 => 'Early Hints',
112              
113             200 => 'OK',
114             201 => 'Created',
115             202 => 'Accepted',
116             203 => 'Non-Authoritative Information',
117             204 => 'No Content',
118             205 => 'Reset Content',
119             206 => 'Partial Content',
120             207 => 'Multi-Status',
121             208 => 'Already Reported',
122             209 => 'IM Used',
123              
124             300 => 'Multiple Choices',
125             301 => 'Moved Permanently',
126             302 => 'Found',
127             303 => 'See Other',
128             304 => 'Not Modified',
129             305 => 'Use Proxy',
130             306 => 'unused',
131             307 => 'Temporary Redirect',
132             308 => 'Permanent Redirect',
133              
134             400 => 'Bad Request',
135             401 => 'Unauthorized',
136             402 => 'Payment Required',
137             403 => 'Forbidden',
138             404 => 'Not Found',
139             405 => 'Method Not Allowed',
140             406 => 'Not Acceptable',
141             407 => 'Proxy Authentication Required',
142             408 => 'Request Timeout',
143             409 => 'Conflict',
144             410 => 'Gone',
145             411 => 'Length Required',
146             412 => 'Precondition Failed',
147             413 => 'Content Too Large',
148             414 => 'URI Too Long',
149             415 => 'Unsupported Media Type',
150             416 => 'Range Not Satisfiable',
151             417 => 'Exception Failed',
152             418 => q{I'm a teapot},
153             421 => 'Misdirected Request',
154             422 => 'Unprocessable Content',
155             423 => 'Locked',
156             424 => 'Failed Dependency',
157             425 => 'Too Early',
158             426 => 'Upgrade Required',
159             428 => 'Precondition Required',
160             429 => 'Too Many Requests',
161             431 => 'Request Header Fields Too Large',
162             451 => 'Unavailable For Legal Reasons',
163              
164             500 => 'Internal Server Error',
165             501 => 'Not Implemented',
166             502 => 'Bad Gateway',
167             503 => 'Service Unavailable',
168             504 => 'Gateway Timeout',
169             505 => 'HTTP Version Not Supported',
170             506 => 'Variant Also Negotiates',
171             507 => 'Insufficient Storage',
172             508 => 'Loop Detected',
173             510 => 'Not Extended',
174             511 => 'Network Authentication Required',
175              
176             );
177              
178             sub curl_error {
179              
180 0     0 1   my ($code) = @_;
181              
182 0   0       return $CODES{ $code } // 'Unknown error';
183              
184             }
185              
186             sub curl {
187              
188 0     0 1   my ($link, $output, %param) = @_;
189 0   0       my $verbose = $param{ verbose } // 0;
190 0   0       my $agent = $param{ agent } // undef;
191 0   0       my $time_cond = $param{ time_cond } // undef;
192 0   0       my $remote_time = $param{ remote_time } // 0;
193 0   0       my $etag_save = $param{ etag_save } // undef;
194 0   0       my $etag_compare = $param{ etag_compare } // undef;
195 0   0       my $limit_rate = $param{ limit_rate } // undef;
196 0   0       my $user_agent = $param{ user_agent } // undef;
197 0   0       my $timeout = $param{ timeout } // undef;
198 0   0       my $fail = $param{ fail } // 0;
199 0   0       my $proxy = $param{ proxy } // undef;
200 0   0       my $proxy_user = $param{ proxy_user } // undef;
201 0   0       my $redirect = $param{ redirect } // 0;
202 0   0       my $compressed = $param{ compressed } // 0;
203              
204 0           my $tmp = do {
205 0           my ($h, $p) = tempfile(UNLINK => 1);
206 0           close $h;
207 0           $p;
208             };
209              
210 0           my @cmd = ('curl', '-D', $tmp, '-o', $output);
211              
212 0 0         if (!$verbose) {
213 0           push @cmd, '-s';
214             }
215              
216 0 0         if (defined $agent) {
217 0           push @cmd, '-A', $agent;
218             }
219              
220 0 0         if (defined $time_cond) {
221 0           push @cmd, '-z', $time_cond;
222             }
223              
224 0 0         if ($remote_time) {
225 0           push @cmd, '-R';
226             }
227              
228 0 0         if (defined $etag_save) {
229 0           push @cmd, '--etag-save', $etag_save;
230             }
231              
232 0 0         if (defined $etag_compare) {
233 0           push @cmd, '--etag-compare', $etag_compare;
234             }
235              
236 0 0         if (defined $limit_rate) {
237 0           push @cmd, '--limit-rate', $limit_rate;
238             }
239              
240 0 0         if (defined $user_agent) {
241 0           push @cmd, '-A', $user_agent;
242             }
243              
244 0 0         if (defined $timeout) {
245 0           push @cmd, '-m', $timeout;
246             }
247              
248 0 0         if ($fail) {
249 0           push @cmd, '-f';
250             }
251              
252 0 0         if (defined $proxy) {
253 0           push @cmd, '-x', $proxy;
254             }
255              
256 0 0         if (defined $proxy_user) {
257 0           push @cmd, '-U', $proxy_user;
258             }
259              
260 0 0         if (defined $redirect) {
261 0           push @cmd, '-L';
262             }
263              
264 0 0         if ($compressed) {
265 0           push @cmd, '--compressed';
266             }
267              
268 0           push @cmd, $link;
269              
270 0           system @cmd;
271              
272 0 0         my $exit = $? == -1 ? $? : $? >> 8;
273             my ($resp, $head) = (
274             -s $tmp
275             # If there are multiple headers, only parse the last one.
276 0 0         ? do {
277 0 0         open my $fh, '<', $tmp or die "Failed to open $tmp for reading: $!\n";
278 0           binmode $fh;
279 0           my $slurp = do { local $/; <$fh> };
  0            
  0            
280 0           close $fh;
281 0           $slurp =~ s/(?:\r\n)*$//;
282 0           my $headstr = (split /(?:\r\n){2,}/, $slurp)[-1];
283 0           parse_http_header($headstr);
284             }
285             : (undef, {})
286             );
287              
288 0           return ($exit, $resp, $head);
289              
290             }
291              
292             sub parse_http_header {
293              
294 0     0 1   my ($header) = @_;
295              
296 0           my ($resp, @lines) = split /\r\n/, $header;
297              
298 0           $resp =~ s/\r\n$//;
299 0           my @resp_parts = split /\s+/, $resp, 3;
300 0 0         if (@resp_parts == 2) {
    0          
301 0           $resp_parts[2] = '';
302             } elsif (@resp_parts != 3) {
303 0           die "invalid HTTP response header";
304             }
305              
306 0           my %head;
307 0           for my $l (@lines) {
308 0           $l =~ s/\r\n$//;
309 0 0         if ($l eq '') {
310 0           last;
311             }
312 0           my ($k, $v) = split /:/, $l, 2;
313 0 0         if (not defined $v) {
314 0           die "invalid HTTP response header";
315             }
316 0           $v =~ s/^\s+|\s+$//g;
317 0           $head{ lc $k } = $v;
318             }
319              
320 0           return (\@resp_parts, \%head);
321              
322             }
323              
324             sub http_status_string {
325              
326 0     0 1   my ($status) = @_;
327              
328 0 0         if (not exists $HTTP_CODES{ $status }) {
329 0           die "'$status' is not a valid HTTP status code";
330             }
331              
332 0           return $HTTP_CODES{ $status };
333              
334             }
335              
336             1;
337              
338             =head1 NAME
339              
340             WWW::Noss::Curl - Interface to curl command
341              
342             =head1 USAGE
343              
344             use WWW::Noss::Curl qw(curl);
345              
346             curl('https://url', 'output');
347              
348             =head1 DESCRIPTION
349              
350             B is a module that provides an interface to the L
351             command for fetching network resources. This is a private module, please
352             consult the L manual for user documentation.
353              
354             =head1 SUBROUTINES
355              
356             Subroutines are not automatically exported.
357              
358             =over 4
359              
360             =item ($rt, \@resp, \%head) = curl($link, $output, [ %param ])
361              
362             L C<$link> and download it to C<$output>. C<%param> is an optional
363             hash argument of additional parameters to pass.
364              
365             Returns the exit code of L. A return value of C<0> means
366             success, non-zero means failure. C can be used to describe
367             the return value. Also returns the return values of C on
368             the reponse's header.
369              
370             The following are valid fields for the C<%param> hash:
371              
372             =over 4
373              
374             =item verbose
375              
376             Boolean determining whether to enable verbose output or not. Corresponds to
377             L's C<--silent> option. Defaults to false.
378              
379             =item agent
380              
381             String to use as user agent. Corresponds to L's C<--user-agent> option.
382             Defaults to none.
383              
384             =item time_cond
385              
386             Only download a file if it has been modified past the given time. Can either
387             be a timestamp or file. Corresponds to L's C<--time-cond> option.
388             Defaults to none.
389              
390             =item remote_time
391              
392             Copy the remote file's modification time when downloading a file. Corresponds
393             to L's C<--remote-time> option. Defaults to false.
394              
395             =item etag_save
396              
397             Path to file to write remote file's etag to, if it has one. Corresponds to
398             L's C<--etag-save> option. Defaults to none.
399              
400             =item etag_compare
401              
402             Path to file to compare remote file's etag to, only downloading the remote file
403             if the etags differ. Corresponds to L's C<--etag-compare> option.
404             Defaults to none.
405              
406             =item limit_rate
407              
408             Download rate to limit L to. Corresponds to L's
409             C<--limit-rate> option. Defaults to none.
410              
411             =item user_agent
412              
413             User agent string to send to server. Corresponds to L's
414             C<--user-agent> option. Defaults to none.
415              
416             =item timeout
417              
418             Maximum time in seconds a transfer is allowed to take. Corresponds to
419             L's C<--max-time> option. Defaults to no timeout.
420              
421             =item fail
422              
423             Boolean determining if L should fail with no output on server errors.
424             Corresponds to L's C<--fail> option. Defaults to false.
425              
426             =item proxy
427              
428             Host to use as proxy. Corresponds to L's C<--proxy> option. Defaults
429             to none.
430              
431             =item proxy_user
432              
433             Username and password to use for proxy, separated by a colon (C).
434             Corresponds to L's C<--proxy-user> option. Defaults to none.
435              
436             =item redirect
437              
438             Boolean determining whether to following redirections. Corresponds to
439             L's C<--location> option. Defaults to false.
440              
441             =item compressed
442              
443             Boolean determining whether to request compressed content and automatically
444             decompress it. Corresponds to L's C<--compressed> option. Defaults to
445             false.
446              
447             =back
448              
449             =item $desc = curl_error($rt)
450              
451             Returns the string description of the C exit code C<$rt>.
452              
453             =item (\@response, \%head) = parse_http_header($header)
454              
455             Parses the HTTP header in C<$header>. C<\@response> is an array ref containing
456             the reponses's information. The first element is the protocol information, the
457             second is the HTTP status code, and the third is a string providing extra
458             information about the reponse. C<\%head> is a hash of data provided by the
459             response's header fields.
460              
461             =item $desc = http_status_string($code)
462              
463             Returns the string description of the HTTP status code C<$code>. Dies if
464             C<$code> is not a valid status code.
465              
466             =back
467              
468             =head1 GLOBAL VARIABLES
469              
470             =over 4
471              
472             =item %WWW::Noss::Curl::CODES
473              
474             Hash of C exit codes and their corresponding string descriptions.
475             Use of the C function is preferable.
476              
477             =back
478              
479             =head1 AUTHOR
480              
481             Written by Samuel Young, Esamyoung12788@gmail.comE.
482              
483             This project's source can be found on its
484             L. Comments and pull
485             requests are welcome!
486              
487             =head1 COPYRIGHT
488              
489             Copyright (C) 2025-2026 Samuel Young
490              
491             This program is free software: you can redistribute it and/or modify
492             it under the terms of the GNU General Public License as published by
493             the Free Software Foundation, either version 3 of the License, or
494             (at your option) any later version.
495              
496             =head1 SEE ALSO
497              
498             L, L
499              
500             =cut
501              
502             # vim: expandtab shiftwidth=4