File Coverage

blib/lib/HTTP/Request/FromWget.pm
Criterion Covered Total %
statement 170 200 85.0
branch 47 76 61.8
condition 5 31 16.1
subroutine 21 21 100.0
pod 2 2 100.0
total 245 330 74.2


line stmt bran cond sub pod time code
1             package HTTP::Request::FromWget;
2 3     3   1455 use strict;
  3         24  
  3         86  
3 3     3   15 use warnings;
  3         6  
  3         70  
4 3     3   1442 use HTTP::Request;
  3         76398  
  3         96  
5 3     3   1580 use HTTP::Request::Common;
  3         7060  
  3         216  
6 3     3   21 use URI;
  3         7  
  3         96  
7 3     3   2140 use Getopt::Long;
  3         31927  
  3         16  
8 3     3   512 use File::Spec::Unix;
  3         7  
  3         131  
9 3     3   1623 use HTTP::Request::CurlParameters;
  3         12  
  3         163  
10 3     3   1697 use HTTP::Request::Generator 'generate_requests';
  3         67323  
  3         204  
11 3     3   27 use PerlX::Maybe;
  3         10  
  3         28  
12 3     3   1619 use MIME::Base64 'encode_base64';
  3         1874  
  3         191  
13              
14 3     3   23 use Filter::signatures;
  3         7  
  3         22  
15 3     3   85 use feature 'signatures';
  3         7  
  3         232  
16 3     3   20 no warnings 'experimental::signatures';
  3         5  
  3         6688  
17              
18             our $VERSION = '0.51';
19              
20             =head1 NAME
21              
22             HTTP::Request::FromWget - create a HTTP::Request from a wget command line
23              
24             =head1 SYNOPSIS
25              
26             my $req = HTTP::Request::FromWget->new(
27             # Note - wget itself may not appear
28             argv => ['https://example.com'],
29             );
30              
31             my $req = HTTP::Request::FromWget->new(
32             command => 'https://example.com',
33             );
34              
35             my $req = HTTP::Request::FromWget->new(
36             command_wget => 'wget -A mywget/1.0 https://example.com',
37             );
38              
39             my @requests = HTTP::Request::FromWget->new(
40             command_wget => 'wget -A mywget/1.0 https://example.com https://www.example.com',
41             );
42             # Send the requests
43             for my $r (@requests) {
44             $ua->request( $r->as_request )
45             }
46              
47             =head1 RATIONALE
48              
49             C command lines are found everywhere in documentation. The Firefox
50             developer tools can also copy network requests as C command lines from
51             the network panel. This module enables converting these to Perl code.
52              
53             =head1 METHODS
54              
55             =head2 C<< ->new >>
56              
57             my $req = HTTP::Request::FromWget->new(
58             # Note - wget itself may not appear
59             argv => ['--user-agent', 'myscript/1.0', 'https://example.com'],
60             );
61              
62             my $req = HTTP::Request::FromWget->new(
63             # Note - wget itself may not appear
64             command => '--user-agent myscript/1.0 https://example.com',
65             );
66              
67             The constructor returns one or more L objects
68             that encapsulate the parameters. If the command generates multiple requests,
69             they will be returned in list context. In scalar context, only the first request
70             will be returned.
71              
72             my $req = HTTP::Request::FromWget->new(
73             command => '--post-file /etc/passwd https://example.com',
74             read_files => 1,
75             );
76              
77             =head3 Options
78              
79             =over 4
80              
81             =item B
82              
83             An arrayref of commands as could be given in C< @ARGV >.
84              
85             =item B
86              
87             A scalar in a command line, excluding the C command
88              
89             =item B
90              
91             A scalar in a command line, including the C command
92              
93             =item B
94              
95             Do read in the content of files specified with (for example)
96             C<< --data=@/etc/passwd >>. The default is to not read the contents of files
97             specified this way.
98              
99             =back
100              
101             =head1 GLOBAL VARIABLES
102              
103             =head2 C<< %default_headers >>
104              
105             Contains the default headers added to every request
106              
107             =cut
108              
109             our %default_headers = (
110             'Accept' => '*/*',
111             'Accept-Encoding' => 'identity',
112             'User-Agent' => 'Wget/1.21',
113             'Connection' => 'Keep-Alive',
114             );
115              
116             =head2 C<< @option_spec >>
117              
118             Contains the L specification of the recognized command line
119             parameters.
120              
121             The following C options are recognized but largely ignored:
122              
123             =over 4
124              
125             =item B
126              
127             =item B
128              
129             =item B
130              
131             =item B
132              
133             =item B
134              
135             If you want to keep session cookies between subsequent requests, you need to
136             provide a cookie jar in your user agent.
137              
138             =back
139              
140             =cut
141              
142             our @option_spec = (
143             'auth-no-challenge', # ignored
144             'bind-address=s',
145             'body-data=s',
146             'body-file=s',
147             'buffer!',
148             'cache!',
149             'ca-directory=s',
150             'check-certificate!',
151             'certificate=s',
152             'compression=s',
153             'content-disposition=s',
154             'cookie|b=s@',
155             'cookies!', # ignored
156             'debug', # ignored
157             'header|H=s@',
158             'http-keep-alive!',
159             'http-password=s',
160             'http-user=s',
161             'load-cookies|c=s',
162             'method=s',
163             'no-verbose|nv', # ignored
164             'output-document|O=s', # ignored
165             'post-data=s',
166             'post-file=s',
167             'progress!', # ignored
168             'quiet', # ignored
169             'referer=s',
170             'timeout|T=i',
171             'user-agent|U=s',
172             'verbose|v', # ignored
173             );
174              
175 40     40 1 2304450 sub new( $class, %options ) {
  40         170  
  40         389  
  40         158  
176 40         134 my $cmd = $options{ argv };
177              
178 40 50       320 if( $options{ command }) {
    50          
179 0         0 require Text::ParseWords;
180 0         0 $cmd = [ Text::ParseWords::shellwords($options{ command }) ];
181              
182             } elsif( $options{ command_wget }) {
183 0         0 require Text::ParseWords;
184 0         0 $cmd = [ Text::ParseWords::shellwords($options{ command_wget }) ];
185              
186             # remove the implicit wget command:
187 0         0 shift @$cmd;
188             };
189              
190 40         812 my $p = Getopt::Long::Parser->new(
191             config => [ 'bundling', 'no_auto_abbrev', 'no_ignore_case_always' ],
192             );
193 40 50       6385 $p->getoptionsfromarray( $cmd,
194             \my %wget_options,
195             @option_spec,
196             ) or return;
197              
198             return
199 40 50       99229 wantarray ? map { $class->_build_request( $_, \%wget_options, %options ) } @$cmd
  40         380  
200             : ($class->_build_request( $cmd->[0], \%wget_options, %options ))[0]
201             ;
202             }
203              
204             =head1 METHODS
205              
206             =head2 C<< ->squash_uri( $uri ) >>
207              
208             my $uri = HTTP::Request::FromWget->squash_uri(
209             URI->new( 'https://example.com/foo/bar/..' )
210             );
211             # https://example.com/foo/
212              
213             Helper method to clean up relative path elements from the URI the same way
214             that wget does.
215              
216             =cut
217              
218 40     40 1 98 sub squash_uri( $class, $uri ) {
  40         92  
  40         78  
  40         65  
219 40         158 my $u = $uri->clone;
220 40         568 my @segments = $u->path_segments;
221              
222 40 0 0     1963 if( $segments[-1] and ($segments[-1] eq '..' or $segments[-1] eq '.' ) ) {
      33        
223 0         0 push @segments, '';
224             };
225              
226 40         100 @segments = grep { $_ ne '.' } @segments;
  80         254  
227              
228             # While we find a pair ( "foo", ".." ) remove that pair
229 40         99 while( grep { $_ eq '..' } @segments ) {
  80         219  
230 0         0 my $i = 0;
231 0         0 while( $i < $#segments ) {
232 0 0 0     0 if( $segments[$i] ne '..' and $segments[$i+1] eq '..') {
233 0         0 splice @segments, $i, 2;
234             } else {
235 0         0 $i++
236             };
237             };
238             };
239              
240 40 50       131 if( @segments < 2 ) {
241 0         0 @segments = ('','');
242             };
243              
244 40         138 $u->path_segments( @segments );
245 40         2648 return $u
246             }
247              
248             # Ugh - wget doesn't allow for multiple headers of the same name on the command line
249 179     179   276 sub _add_header( $self, $headers, $h, $value ) {
  179         257  
  179         251  
  179         234  
  179         256  
  179         218  
250             #if( exists $headers->{ $h }) {
251             # if (!ref( $headers->{ $h })) {
252             # $headers->{ $h } = [ $headers->{ $h }];
253             # }
254             # push @{ $headers->{ $h } }, $value;
255             #} else {
256 179         442 $headers->{ $h } = $value;
257             #}
258             }
259              
260 25     25   78 sub _set_header( $self, $headers, $h, $value ) {
  25         53  
  25         48  
  25         101  
  25         69  
  25         43  
261 25         86 $headers->{ $h } = $value;
262             }
263              
264 48     48   116 sub _maybe_set_header( $self, $headers, $h, $value ) {
  48         107  
  48         80  
  48         141  
  48         97  
  48         65  
265 48 100       157 if( ! exists $headers->{ $h }) {
266 42         128 $headers->{ $h } = $value;
267             };
268             }
269              
270 1     1   6 sub _maybe_read_data_file( $self, $read_files, $data ) {
  1         4  
  1         9  
  1         9  
  1         4  
271 1         209 my $res;
272 1 50       8 if( $read_files ) {
273 1 50       60 open my $fh, '<', $data
274             or die "$data: $!";
275 1         7 local $/; # / for Filter::Simple
276 1         7 binmode $fh;
277 1         50 $res = <$fh>
278             } else {
279 0         0 $res = "... contents of $data ..."
280             }
281 1         7 return $res
282             }
283              
284 40     40   118 sub _build_request( $self, $uri, $options, %build_options ) {
  40         90  
  40         114  
  40         85  
  40         130  
  40         78  
285 40         78 my $body;
286              
287 40 100       74 my @headers = @{ $options->{header} || []};
  40         335  
288 40         136 my $method = $options->{method};
289              
290             # Ideally, we shouldn't sort the data but process it in-order
291 40         70 my @post_raw_data;
292 40 100       137 if( exists $options->{ 'post-data' }) {
293 3         14 @post_raw_data = $options->{'post-data'};
294 3         13 $method = 'POST';
295             };
296 40 100       124 if( exists $options->{ 'body-data' }) {
297 2         19 @post_raw_data = $options->{'body-data'};
298 2   50     16 $method ||= 'POST';
299             };
300             ;
301 40 100       120 if( my $file = $options->{'post-file'} ) {
302 1         67 @post_raw_data = $self->_maybe_read_data_file( $build_options{ read_files }, $file );
303 1         5 $method = 'POST';
304             };
305 40 50       149 if( my $file = $options->{'body-file'} ) {
306 0         0 @post_raw_data = $self->_maybe_read_data_file( $build_options{ read_files }, $file );
307 0   0     0 $method ||= 'POST';
308             };
309             ;
310 40 50       75 my @form_args = @{ $options->{form} || []};
  40         344  
311              
312             # expand the URI here if wanted
313 40         127 my @uris = ($uri);
314 40 50       125 if( ! $options->{ globoff }) {
315 40         530 @uris = map { $_->{url} } generate_requests( pattern => shift @uris, limit => $build_options{ limit } );
  40         48437  
316             }
317              
318 40         186 my @res;
319 40         116 for my $uri (@uris) {
320 40         160 $uri = URI->new( $uri );
321 40         3705 $uri = $self->squash_uri( $uri );
322              
323 40 50       338 my $host = $uri->can( 'host_port' ) ? $uri->host_port : "$uri";
324              
325             # Stuff we use unless nothing else hits
326 40         1487 my %request_default_headers = %default_headers;
327              
328 40         109 my $data;
329 40 100       128 if( @post_raw_data ) {
330 6         27 $data = join "&",
331             @post_raw_data,
332             ;
333             };
334              
335 40 50       163 if( @form_args) {
    100          
336 0   0     0 $method ||= 'POST';
337              
338             my $req = HTTP::Request::Common::POST(
339             'https://example.com',
340             Content_Type => 'form-data',
341 0 0       0 Content => [ map { /^([^=]+)=(.*)$/ ? ($1 => $2) : () } @form_args ],
  0         0  
342             );
343 0         0 $body = $req->content;
344 0         0 $request_default_headers{ 'Content-Type' } = join "; ", $req->headers->content_type;
345              
346             } elsif( defined $data ) {
347 6   50     27 $method ||= 'POST';
348 6         15 $body = $data;
349 6         22 $request_default_headers{ 'Content-Type' } = 'application/x-www-form-urlencoded';
350              
351             } else {
352 34   50     289 $method ||= 'GET';
353             };
354              
355 40 100       132 if( defined $body ) {
356 6         23 $request_default_headers{ 'Content-Length' } = length $body;
357             };
358              
359 40 50 33     597 if( $options->{ 'user' } || $options->{'http-user'} ) {
360 0 0 0     0 if( $options->{anyauth}
      0        
361             || $options->{ntlm}
362             || $options->{negotiate}
363             ) {
364             # Nothing to do here, just let LWP::UserAgent do its thing
365             # This means one additional request to fetch the appropriate
366             # 401 response asking for credentials, but ...
367             } else {
368             # $options->{basic} or none at all
369 0   0     0 my $info = delete $options->{'user'} || delete $options->{'http-user'};
370             # We need to bake this into the header here?!
371 0         0 push @headers, sprintf 'Authorization: Basic %s', encode_base64( $info );
372             }
373             };
374              
375 40         175 my %headers;
376 40         129 for my $kv (
377 40 50       410 (map { /^\s*([^:\s]+)\s*:\s*(.*)$/ ? [$1 => $2] : () } @headers),) {
378 40         163 $self->_add_header( \%headers, @$kv );
379             };
380              
381 40 100       162 if( defined $options->{ 'user-agent' }) {
382 23         139 $self->_set_header( \%headers, "User-Agent", $options->{ 'user-agent' } );
383             };
384              
385 40 100       115 if( exists $options->{ 'cache' }) {
386 5 100       16 if(! $options->{ 'cache' } ) {
387 4         32 $self->_maybe_set_header( \%headers, "Cache-Control" => 'no-cache' );
388 4         13 $self->_maybe_set_header( \%headers, "Pragma" => 'no-cache' );
389             };
390             };
391              
392 40 100       120 if( exists $options->{ 'http-keep-alive' }) {
393 2 100       12 if(! $options->{ 'http-keep-alive' } ) {
394 1         23 $self->_set_header( \%headers, "Connection" => 'Close' );
395             };
396             };
397              
398 40 100       125 if( defined $options->{ referer }) {
399 1         26 $self->_set_header( \%headers, "Referer" => $options->{ 'referer' } );
400             };
401              
402             # We want to compare the headers case-insensitively
403 40         142 my %headers_lc = map { lc $_ => 1 } keys %headers;
  69         244  
404              
405 40         155 for my $k (keys %request_default_headers) {
406 172 100       442 if( ! $headers_lc{ lc $k }) {
407 139         339 $self->_add_header( \%headers, $k, $request_default_headers{ $k });
408             };
409             };
410 40         202 $self->_maybe_set_header( \%headers, 'Host' => $host );
411              
412 40 50       122 if( defined $options->{ 'cookie-jar' }) {
413 0         0 $options->{'cookie-jar-options'}->{ 'write' } = 1;
414             };
415              
416 40 50       116 if( defined( my $c = $options->{ cookie })) {
417 0 0       0 if( $c =~ /=/ ) {
418 0         0 $headers{ Cookie } = $options->{ 'cookie' };
419             } else {
420 0         0 $options->{'cookie-jar'} = $c;
421 0         0 $options->{'cookie-jar-options'}->{ 'read' } = 1;
422             };
423             };
424              
425 40 50       111 if( my $c = $options->{ compression }) {
426 0 0       0 if( $c =~ /^(gzip|auto)$/ ) {
427             # my $compressions = HTTP::Message::decodable();
428 0         0 $self->_set_header( \%headers, 'Accept-Encoding' => 'gzip' );
429             };
430             };
431              
432             push @res, HTTP::Request::CurlParameters->new({
433             method => $method,
434             uri => $uri,
435             headers => \%headers,
436             body => $body,
437             maybe local_address => $options->{local_address},
438             maybe cert => $options->{certificate},
439             maybe capath => $options->{'ca-directory'},
440             maybe credentials => $options->{ user },
441             maybe output => $options->{ output },
442             maybe timeout => $options->{ 'max-time' },
443             maybe cookie_jar => $options->{'cookie-jar'},
444             maybe cookie_jar_options => $options->{'cookie-jar-options'},
445             maybe insecure => !$options->{'check-certificate'},
446             maybe show_error => $options->{'show_error'},
447 40         2210 maybe fail => $options->{'fail'},
448             });
449             }
450              
451             return @res
452 40         1096 };
453              
454             1;
455              
456             =head1 LIVE DEMO
457              
458             L
459              
460             =head1 KNOWN DIFFERENCES
461              
462             =head2 Incompatible cookie jar formats
463              
464             Until somebody writes a robust Netscape cookie file parser and proper loading
465             and storage for L, this module will not be able to load and
466             save files in the format that wget uses.
467              
468             =head2 Loading/saving cookie jars is the job of the UA
469              
470             You're expected to instruct your UA to load/save cookie jars:
471              
472             use Path::Tiny;
473             use HTTP::CookieJar::LWP;
474              
475             if( my $cookies = $r->cookie_jar ) {
476             $ua->cookie_jar( HTTP::CookieJar::LWP->new()->load_cookies(
477             path($cookies)->lines
478             ));
479             };
480              
481             =head2 Different Content-Length for POST requests
482              
483             =head2 Different delimiter for form data
484              
485             The delimiter is built by L, and C uses a different
486             mechanism to come up with a unique data delimiter. This results in differences
487             in the raw body content and the C header.
488              
489             =head1 MISSING FUNCTIONALITY
490              
491             =over 4
492              
493             =item *
494              
495             File uploads / content from files
496              
497             While file uploads and reading POST data from files are supported, the content
498             is slurped into memory completely. This can be problematic for large files
499             and little available memory.
500              
501              
502             =back
503              
504             =head1 SEE ALSO
505              
506             L - for the inverse function
507              
508             The module HTTP::Request::AsCurl likely also implements a much better version
509             of C<< ->as_curl >> than this module.
510              
511             =head1 REPOSITORY
512              
513             The public repository of this module is
514             L.
515              
516             =head1 SUPPORT
517              
518             The public support forum of this module is
519             L.
520              
521             =head1 BUG TRACKER
522              
523             Please report bugs in this module via the Github bug queue at
524             L
525              
526             =head1 AUTHOR
527              
528             Max Maischein C
529              
530             =head1 COPYRIGHT (c)
531              
532             Copyright 2018-2023 by Max Maischein C.
533              
534             =head1 LICENSE
535              
536             This module is released under the same terms as Perl itself.
537              
538             =cut