File Coverage

blib/lib/HTTP/Request/FromWget.pm
Criterion Covered Total %
statement 168 200 84.0
branch 46 76 60.5
condition 5 31 16.1
subroutine 21 21 100.0
pod 2 2 100.0
total 242 330 73.3


line stmt bran cond sub pod time code
1             package HTTP::Request::FromWget;
2 3     3   1472 use strict;
  3         25  
  3         88  
3 3     3   19 use warnings;
  3         5  
  3         77  
4 3     3   1449 use HTTP::Request;
  3         79674  
  3         92  
5 3     3   1566 use HTTP::Request::Common;
  3         7339  
  3         240  
6 3     3   24 use URI;
  3         5  
  3         66  
7 3     3   2419 use Getopt::Long;
  3         32921  
  3         12  
8 3     3   470 use File::Spec::Unix;
  3         7  
  3         135  
9 3     3   1696 use HTTP::Request::CurlParameters;
  3         11  
  3         146  
10 3     3   1725 use HTTP::Request::Generator 'generate_requests';
  3         69660  
  3         189  
11 3     3   23 use PerlX::Maybe;
  3         6  
  3         28  
12 3     3   1533 use MIME::Base64 'encode_base64';
  3         1935  
  3         206  
13              
14 3     3   23 use Filter::signatures;
  3         8  
  3         18  
15 3     3   90 use feature 'signatures';
  3         10  
  3         212  
16 3     3   18 no warnings 'experimental::signatures';
  3         6  
  3         6778  
17              
18             our $VERSION = '0.52';
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 34     34 1 2278820 sub new( $class, %options ) {
  34         157  
  34         300  
  34         93  
176 34         168 my $cmd = $options{ argv };
177              
178 34 50       323 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 34         907 my $p = Getopt::Long::Parser->new(
191             config => [ 'bundling', 'no_auto_abbrev', 'no_ignore_case_always' ],
192             );
193 34 50       6452 $p->getoptionsfromarray( $cmd,
194             \my %wget_options,
195             @option_spec,
196             ) or return;
197              
198             return
199 34 50       92105 wantarray ? map { $class->_build_request( $_, \%wget_options, %options ) } @$cmd
  34         377  
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 34     34 1 94 sub squash_uri( $class, $uri ) {
  34         89  
  34         88  
  34         67  
219 34         157 my $u = $uri->clone;
220 34         530 my @segments = $u->path_segments;
221              
222 34 0 0     1919 if( $segments[-1] and ($segments[-1] eq '..' or $segments[-1] eq '.' ) ) {
      33        
223 0         0 push @segments, '';
224             };
225              
226 34         109 @segments = grep { $_ ne '.' } @segments;
  68         207  
227              
228             # While we find a pair ( "foo", ".." ) remove that pair
229 34         106 while( grep { $_ eq '..' } @segments ) {
  68         449  
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 34 50       126 if( @segments < 2 ) {
241 0         0 @segments = ('','');
242             };
243              
244 34         131 $u->path_segments( @segments );
245 34         2408 return $u
246             }
247              
248             # Ugh - wget doesn't allow for multiple headers of the same name on the command line
249 145     145   247 sub _add_header( $self, $headers, $h, $value ) {
  145         228  
  145         201  
  145         221  
  145         219  
  145         183  
250             #if( exists $headers->{ $h }) {
251             # if (!ref( $headers->{ $h })) {
252             # $headers->{ $h } = [ $headers->{ $h }];
253             # }
254             # push @{ $headers->{ $h } }, $value;
255             #} else {
256 145         395 $headers->{ $h } = $value;
257             #}
258             }
259              
260 22     22   65 sub _set_header( $self, $headers, $h, $value ) {
  22         51  
  22         51  
  22         91  
  22         52  
  22         39  
261 22         93 $headers->{ $h } = $value;
262             }
263              
264 34     34   69 sub _maybe_set_header( $self, $headers, $h, $value ) {
  34         60  
  34         58  
  34         96  
  34         72  
  34         58  
265 34 100       122 if( ! exists $headers->{ $h }) {
266 30         96 $headers->{ $h } = $value;
267             };
268             }
269              
270 1     1   7 sub _maybe_read_data_file( $self, $read_files, $data ) {
  1         3  
  1         11  
  1         7  
  1         5  
271 1         6 my $res;
272 1 50       18 if( $read_files ) {
273 1 50       66 open my $fh, '<', $data
274             or die "$data: $!";
275 1         187 local $/; # / for Filter::Simple
276 1         5 binmode $fh;
277 1         41 $res = <$fh>
278             } else {
279 0         0 $res = "... contents of $data ..."
280             }
281 1         9 return $res
282             }
283              
284 34     34   92 sub _build_request( $self, $uri, $options, %build_options ) {
  34         88  
  34         94  
  34         68  
  34         114  
  34         70  
285 34         88 my $body;
286              
287 34 100       78 my @headers = @{ $options->{header} || []};
  34         335  
288 34         157 my $method = $options->{method};
289              
290             # Ideally, we shouldn't sort the data but process it in-order
291 34         78 my @post_raw_data;
292 34 100       157 if( exists $options->{ 'post-data' }) {
293 3         28 @post_raw_data = $options->{'post-data'};
294 3         10 $method = 'POST';
295             };
296 34 100       124 if( exists $options->{ 'body-data' }) {
297 2         20 @post_raw_data = $options->{'body-data'};
298 2   50     20 $method ||= 'POST';
299             };
300             ;
301 34 100       148 if( my $file = $options->{'post-file'} ) {
302 1         27 @post_raw_data = $self->_maybe_read_data_file( $build_options{ read_files }, $file );
303 1         8 $method = 'POST';
304             };
305 34 50       146 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 34 50       68 my @form_args = @{ $options->{form} || []};
  34         309  
311              
312             # expand the URI here if wanted
313 34         119 my @uris = ($uri);
314 34 50       140 if( ! $options->{ globoff }) {
315 34         501 @uris = map { $_->{url} } generate_requests( pattern => shift @uris, limit => $build_options{ limit } );
  34         46202  
316             }
317              
318 34         170 my @res;
319 34         138 for my $uri (@uris) {
320 34         151 $uri = URI->new( $uri );
321 34         3443 $uri = $self->squash_uri( $uri );
322              
323 34 50       323 my $host = $uri->can( 'host_port' ) ? $uri->host_port : "$uri";
324              
325             # Stuff we use unless nothing else hits
326 34         1276 my %request_default_headers = %default_headers;
327              
328 34         105 my $data;
329 34 100       151 if( @post_raw_data ) {
330 6         42 $data = join "&",
331             @post_raw_data,
332             ;
333             };
334              
335 34 50       186 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     50 $method ||= 'POST';
348 6         18 $body = $data;
349 6         37 $request_default_headers{ 'Content-Type' } = 'application/x-www-form-urlencoded';
350              
351             } else {
352 28   50     262 $method ||= 'GET';
353             };
354              
355 34 100       165 if( defined $body ) {
356 6         24 $request_default_headers{ 'Content-Length' } = length $body;
357             };
358              
359 34 50 33     375 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 34         96 my %headers;
376 34         101 for my $kv (
377 27 50       250 (map { /^\s*([^:\s]+)\s*:\s*(.*)$/ ? [$1 => $2] : () } @headers),) {
378 27         126 $self->_add_header( \%headers, @$kv );
379             };
380              
381 34 100       171 if( defined $options->{ 'user-agent' }) {
382 20         210 $self->_set_header( \%headers, "User-Agent", $options->{ 'user-agent' } );
383             };
384              
385 34 100       112 if( exists $options->{ 'cache' }) {
386 1 50       5 if(! $options->{ 'cache' } ) {
387 0         0 $self->_maybe_set_header( \%headers, "Cache-Control" => 'no-cache' );
388 0         0 $self->_maybe_set_header( \%headers, "Pragma" => 'no-cache' );
389             };
390             };
391              
392 34 100       96 if( exists $options->{ 'http-keep-alive' }) {
393 2 100       48 if(! $options->{ 'http-keep-alive' } ) {
394 1         26 $self->_set_header( \%headers, "Connection" => 'Close' );
395             };
396             };
397              
398 34 100       106 if( defined $options->{ referer }) {
399 1         20 $self->_set_header( \%headers, "Referer" => $options->{ 'referer' } );
400             };
401              
402             # We want to compare the headers case-insensitively
403 34         122 my %headers_lc = map { lc $_ => 1 } keys %headers;
  48         191  
404              
405 34         140 for my $k (keys %request_default_headers) {
406 148 100       390 if( ! $headers_lc{ lc $k }) {
407 118         349 $self->_add_header( \%headers, $k, $request_default_headers{ $k });
408             };
409             };
410 34         234 $self->_maybe_set_header( \%headers, 'Host' => $host );
411              
412 34 50       117 if( defined $options->{ 'cookie-jar' }) {
413 0         0 $options->{'cookie-jar-options'}->{ 'write' } = 1;
414             };
415              
416 34 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 34 50       106 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 34         1985 maybe fail => $options->{'fail'},
448             });
449             }
450              
451             return @res
452 34         1112 };
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