File Coverage

blib/lib/HTTP/Request/FromWget.pm
Criterion Covered Total %
statement 35 193 18.1
branch 0 76 0.0
condition 0 31 0.0
subroutine 12 19 63.1
pod 2 2 100.0
total 49 321 15.2


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