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   1471 use strict;
  3         22  
  3         99  
3 3     3   16 use warnings;
  3         5  
  3         85  
4 3     3   1347 use HTTP::Request;
  3         76306  
  3         99  
5 3     3   1494 use HTTP::Request::Common;
  3         6926  
  3         251  
6 3     3   21 use URI;
  3         6  
  3         108  
7 3     3   2239 use Getopt::Long;
  3         31768  
  3         14  
8 3     3   457 use File::Spec::Unix;
  3         6  
  3         132  
9 3     3   1622 use HTTP::Request::CurlParameters;
  3         13  
  3         129  
10 3     3   1659 use HTTP::Request::Generator 'generate_requests';
  3         66942  
  3         217  
11 3     3   24 use PerlX::Maybe;
  3         7  
  3         27  
12 3     3   1552 use MIME::Base64 'encode_base64';
  3         1896  
  3         210  
13              
14 3     3   21 use Filter::signatures;
  3         7  
  3         20  
15 3     3   101 use feature 'signatures';
  3         6  
  3         205  
16 3     3   16 no warnings 'experimental::signatures';
  3         6  
  3         6749  
17              
18             our $VERSION = '0.50';
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 2402643 sub new( $class, %options ) {
  40         176  
  40         425  
  40         139  
176 40         112 my $cmd = $options{ argv };
177              
178 40 50       310 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         874 my $p = Getopt::Long::Parser->new(
191             config => [ 'bundling', 'no_auto_abbrev', 'no_ignore_case_always' ],
192             );
193 40 50       6990 $p->getoptionsfromarray( $cmd,
194             \my %wget_options,
195             @option_spec,
196             ) or return;
197              
198             return
199 40 50       102045 wantarray ? map { $class->_build_request( $_, \%wget_options, %options ) } @$cmd
  40         340  
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 92 sub squash_uri( $class, $uri ) {
  40         98  
  40         86  
  40         69  
219 40         136 my $u = $uri->clone;
220 40         500 my @segments = $u->path_segments;
221              
222 40 0 0     1908 if( $segments[-1] and ($segments[-1] eq '..' or $segments[-1] eq '.' ) ) {
      33        
223 0         0 push @segments, '';
224             };
225              
226 40         98 @segments = grep { $_ ne '.' } @segments;
  80         234  
227              
228             # While we find a pair ( "foo", ".." ) remove that pair
229 40         95 while( grep { $_ eq '..' } @segments ) {
  80         251  
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       155 if( @segments < 2 ) {
241 0         0 @segments = ('','');
242             };
243              
244 40         143 $u->path_segments( @segments );
245 40         2733 return $u
246             }
247              
248             # Ugh - wget doesn't allow for multiple headers of the same name on the command line
249 179     179   266 sub _add_header( $self, $headers, $h, $value ) {
  179         294  
  179         241  
  179         257  
  179         256  
  179         249  
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         445 $headers->{ $h } = $value;
257             #}
258             }
259              
260 25     25   62 sub _set_header( $self, $headers, $h, $value ) {
  25         70  
  25         47  
  25         52  
  25         44  
  25         45  
261 25         85 $headers->{ $h } = $value;
262             }
263              
264 48     48   84 sub _maybe_set_header( $self, $headers, $h, $value ) {
  48         101  
  48         77  
  48         94  
  48         80  
  48         80  
265 48 100       154 if( ! exists $headers->{ $h }) {
266 42         136 $headers->{ $h } = $value;
267             };
268             }
269              
270 1     1   8 sub _maybe_read_data_file( $self, $read_files, $data ) {
  1         9  
  1         11  
  1         3  
  1         8  
271 1         185 my $res;
272 1 50       9 if( $read_files ) {
273 1 50       61 open my $fh, '<', $data
274             or die "$data: $!";
275 1         9 local $/; # / for Filter::Simple
276 1         5 binmode $fh;
277 1         37 $res = <$fh>
278             } else {
279 0         0 $res = "... contents of $data ..."
280             }
281 1         11 return $res
282             }
283              
284 40     40   125 sub _build_request( $self, $uri, $options, %build_options ) {
  40         91  
  40         115  
  40         91  
  40         105  
  40         83  
285 40         72 my $body;
286              
287 40 100       76 my @headers = @{ $options->{header} || []};
  40         318  
288 40         140 my $method = $options->{method};
289              
290             # Ideally, we shouldn't sort the data but process it in-order
291 40         94 my @post_raw_data;
292 40 100       152 if( exists $options->{ 'post-data' }) {
293 3         9 @post_raw_data = $options->{'post-data'};
294 3         13 $method = 'POST';
295             };
296 40 100       129 if( exists $options->{ 'body-data' }) {
297 2         19 @post_raw_data = $options->{'body-data'};
298 2   50     12 $method ||= 'POST';
299             };
300             ;
301 40 100       131 if( my $file = $options->{'post-file'} ) {
302 1         26 @post_raw_data = $self->_maybe_read_data_file( $build_options{ read_files }, $file );
303 1         8 $method = 'POST';
304             };
305 40 50       144 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       87 my @form_args = @{ $options->{form} || []};
  40         378  
311              
312             # expand the URI here if wanted
313 40         162 my @uris = ($uri);
314 40 50       143 if( ! $options->{ globoff }) {
315 40         501 @uris = map { $_->{url} } generate_requests( pattern => shift @uris, limit => $build_options{ limit } );
  40         49860  
316             }
317              
318 40         192 my @res;
319 40         138 for my $uri (@uris) {
320 40         166 $uri = URI->new( $uri );
321 40         3717 $uri = $self->squash_uri( $uri );
322              
323 40 50       335 my $host = $uri->can( 'host_port' ) ? $uri->host_port : "$uri";
324              
325             # Stuff we use unless nothing else hits
326 40         1443 my %request_default_headers = %default_headers;
327              
328 40         99 my $data;
329 40 100       143 if( @post_raw_data ) {
330 6         26 $data = join "&",
331             @post_raw_data,
332             ;
333             };
334              
335 40 50       182 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     24 $method ||= 'POST';
348 6         23 $body = $data;
349 6         29 $request_default_headers{ 'Content-Type' } = 'application/x-www-form-urlencoded';
350              
351             } else {
352 34   50     314 $method ||= 'GET';
353             };
354              
355 40 100       173 if( defined $body ) {
356 6         22 $request_default_headers{ 'Content-Length' } = length $body;
357             };
358              
359 40 50 33     349 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         164 my %headers;
376 40         168 for my $kv (
377 40 50       366 (map { /^\s*([^:\s]+)\s*:\s*(.*)$/ ? [$1 => $2] : () } @headers),) {
378 40         168 $self->_add_header( \%headers, @$kv );
379             };
380              
381 40 100       182 if( defined $options->{ 'user-agent' }) {
382 23         167 $self->_set_header( \%headers, "User-Agent", $options->{ 'user-agent' } );
383             };
384              
385 40 100       118 if( exists $options->{ 'cache' }) {
386 5 100       21 if(! $options->{ 'cache' } ) {
387 4         26 $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       109 if( exists $options->{ 'http-keep-alive' }) {
393 2 100       7 if(! $options->{ 'http-keep-alive' } ) {
394 1         19 $self->_set_header( \%headers, "Connection" => 'Close' );
395             };
396             };
397              
398 40 100       117 if( defined $options->{ referer }) {
399 1         16 $self->_set_header( \%headers, "Referer" => $options->{ 'referer' } );
400             };
401              
402             # We want to compare the headers case-insensitively
403 40         167 my %headers_lc = map { lc $_ => 1 } keys %headers;
  69         247  
404              
405 40         163 for my $k (keys %request_default_headers) {
406 172 100       434 if( ! $headers_lc{ lc $k }) {
407 139         349 $self->_add_header( \%headers, $k, $request_default_headers{ $k });
408             };
409             };
410 40         219 $self->_maybe_set_header( \%headers, 'Host' => $host );
411              
412 40 50       133 if( defined $options->{ 'cookie-jar' }) {
413 0         0 $options->{'cookie-jar-options'}->{ 'write' } = 1;
414             };
415              
416 40 50       122 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       109 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         2151 maybe fail => $options->{'fail'},
448             });
449             }
450              
451             return @res
452 40         1097 };
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