File Coverage

blib/lib/HTTP/Request/FromCurl.pm
Criterion Covered Total %
statement 244 256 95.3
branch 107 128 83.5
condition 33 42 78.5
subroutine 21 21 100.0
pod 2 2 100.0
total 407 449 90.6


line stmt bran cond sub pod time code
1             package HTTP::Request::FromCurl 0.57;
2 14     14   308622 use 5.020;
  14         41  
3 14     14   78 use File::Basename 'basename';
  14         23  
  14         1190  
4 14     14   4770 use HTTP::Request;
  14         272377  
  14         429  
5 14     14   6424 use HTTP::Request::Common;
  14         31527  
  14         1109  
6 14     14   84 use URI;
  14         25  
  14         289  
7 14     14   49 use URI::Escape;
  14         18  
  14         606  
8 14     14   8488 use Getopt::Long;
  14         142114  
  14         54  
9 14     14   2022 use File::Spec::Unix;
  14         33  
  14         440  
10 14     14   6773 use HTTP::Request::CurlParameters;
  14         47  
  14         568  
11 14     14   6771 use HTTP::Request::Generator 'generate_requests';
  14         514728  
  14         968  
12 14     14   158 use PerlX::Maybe;
  14         35  
  14         89  
13 14     14   5997 use MIME::Base64 'encode_base64';
  14         8704  
  14         933  
14 14     14   88 use File::Basename 'basename';
  14         19  
  14         512  
15              
16 14     14   54 use feature 'signatures';
  14         20  
  14         1664  
17 14     14   62 no warnings 'experimental::signatures';
  14         17  
  14         49935  
18              
19             =head1 NAME
20              
21             HTTP::Request::FromCurl - create a HTTP::Request from a curl command line
22              
23             =head1 SYNOPSIS
24              
25             my $req = HTTP::Request::FromCurl->new(
26             # Note - curl itself may not appear
27             argv => ['https://example.com'],
28             );
29              
30             my $req = HTTP::Request::FromCurl->new(
31             command => 'https://example.com',
32             );
33              
34             my $req = HTTP::Request::FromCurl->new(
35             command_curl => 'curl -A mycurl/1.0 https://example.com',
36             );
37              
38             my @requests = HTTP::Request::FromCurl->new(
39             command_curl => 'curl -A mycurl/1.0 https://example.com https://www.example.com',
40             );
41             # Send the requests
42             for my $r (@requests) {
43             $ua->request( $r->as_request )
44             }
45              
46             =head1 RATIONALE
47              
48             C command lines are found everywhere in documentation. The Firefox
49             developer tools can also copy network requests as C command lines from
50             the network panel. This module enables converting these to Perl code.
51              
52             =head1 METHODS
53              
54             =head2 C<< ->new >>
55              
56             my $req = HTTP::Request::FromCurl->new(
57             # Note - curl itself may not appear
58             argv => ['--user-agent', 'myscript/1.0', 'https://example.com'],
59             );
60              
61             my $req = HTTP::Request::FromCurl->new(
62             # Note - curl itself may not appear
63             command => '--user-agent myscript/1.0 https://example.com',
64             );
65              
66             The constructor returns one or more L objects
67             that encapsulate the parameters. If the command generates multiple requests,
68             they will be returned in list context. In scalar context, only the first request
69             will be returned. Note that the order of URLs between C<--url> and unadorned URLs will be changed in the sense that all unadorned URLs will be handled first.
70              
71             my $req = HTTP::Request::FromCurl->new(
72             command => '--data-binary @/etc/passwd https://example.com',
73             read_files => 1,
74             );
75              
76             =head3 Options
77              
78             =over 4
79              
80             =item B
81              
82             An arrayref of commands as could be given in C< @ARGV >.
83              
84             =item B
85              
86             A scalar in a command line, excluding the C command
87              
88             =item B
89              
90             A scalar in a command line, including the C command
91              
92             =item B
93              
94             Do read in the content of files specified with (for example)
95             C<< --data=@/etc/passwd >>. The default is to not read the contents of files
96             specified this way.
97              
98             =back
99              
100             =head1 GLOBAL VARIABLES
101              
102             =head2 C<< %default_headers >>
103              
104             Contains the default headers added to every request
105              
106             =cut
107              
108             our %default_headers = (
109             'Accept' => '*/*',
110             'User-Agent' => 'curl/7.55.1',
111             );
112              
113             =head2 C<< @option_spec >>
114              
115             Contains the L specification of the recognized command line
116             parameters.
117              
118             The following C options are recognized but largely ignored:
119              
120             =over 4
121              
122             =item C< --disable >
123              
124             =item C< --dump-header >
125              
126             =item C< --include >
127              
128             =item C< --follow >
129              
130             =item C< --location >
131              
132             =item C< --progress-bar >
133              
134             =item C< --show-error >
135              
136             =item C< --fail >
137              
138             =item C< --silent >
139              
140             =item C< --verbose >
141              
142             =item C< --junk-session-cookies >
143              
144             If you want to keep session cookies between subsequent requests, you need to
145             provide a cookie jar in your user agent.
146              
147             =item C<--next>
148              
149             Resetting the UA between requests is something you need to handle yourself
150              
151             =item C<--parallel>
152              
153             =item C<--parallel-immediate>
154              
155             =item C<--parallel-max>
156              
157             Parallel requests is something you need to handle in the UA
158              
159             =back
160              
161             =cut
162              
163             our @option_spec = (
164             'user-agent|A=s',
165             'verbose|v', # ignored
166             'show-error|S', # ignored
167             'fail|f',
168             'silent|s', # ignored
169             'anyauth', # ignored
170             'basic',
171             'buffer!',
172             'capath=s',
173             # 'cacert=s', # to be added
174             'cert|E=s', # this is the client certificate
175             'compressed',
176             'cookie|b=s',
177             'cookie-jar|c=s',
178             'data|d=s@',
179             'data-ascii=s@',
180             'data-binary=s@',
181             'data-raw=s@',
182             'data-urlencode=s@',
183             'digest',
184             'disable|q!', # ignored
185             'dump-header|D=s', # ignored
186             'referrer|e=s',
187             'follow', # ignored, we always follow redirects
188             'form|F=s@',
189             'form-string=s@',
190             'get|G',
191             'globoff|g',
192             'head|I',
193             'header|H=s@',
194             'include|i', # ignored
195             'interface=s',
196             'insecure|k',
197             'json=s@',
198             'location|L', # ignored, we always follow redirects
199             'max-filesize=s',
200             'max-time|m=s',
201             'ntlm',
202             'keepalive!',
203             'range=s',
204             'request|X=s',
205             'oauth2-bearer=s',
206             'output|o=s',
207             'progress-bar|#', # ignored
208             'user|u=s',
209             'next', # ignored
210             'parallel|Z', # ignored
211             'parallel-immediate', # ignored
212             'parallel-max=i', # ignored
213             'junk-session-cookies|j', # ignored, must be set in code using the HTTP request
214             'unix-socket=s',
215             'url=s@',
216             );
217              
218 151     151 1 38693828 sub new( $class, %options ) {
  151         588  
  151         1080  
  151         324  
219 151         432 my $cmd = $options{ argv };
220              
221 151 100       1147 if( $options{ command }) {
    50          
222 1         401 require Text::ParseWords;
223 1         1321 $cmd = [ Text::ParseWords::shellwords($options{ command }) ];
224              
225             } elsif( $options{ command_curl }) {
226 0         0 require Text::ParseWords;
227 0         0 $cmd = [ Text::ParseWords::shellwords($options{ command_curl }) ];
228              
229             # remove the implicit curl command:
230 0         0 shift @$cmd;
231             };
232              
233 151         852 for (@$cmd) {
234 930 100       1907 $_ = '--next'
235             if $_ eq '-:'; # GetOptions does not like "next|:" as specification
236             };
237              
238 151         2208 my $p = Getopt::Long::Parser->new(
239             config => [ 'bundling', 'no_auto_abbrev', 'no_ignore_case_always' ],
240             );
241 151 50       33765 $p->getoptionsfromarray( $cmd,
242             \my %curl_options,
243             @option_spec,
244             ) or return;
245 151 100       448089 my @urls = (@$cmd, @{ $curl_options{ url } || [] });
  151         1257  
246              
247             return
248 151 100       593 wantarray ? map { $class->_build_request( $_, \%curl_options, %options ) } @urls
  160         883  
249             : ($class->_build_request( $urls[0], \%curl_options, %options ))[0]
250             ;
251             }
252              
253             =head1 METHODS
254              
255             =head2 C<< ->squash_uri( $uri ) >>
256              
257             my $uri = HTTP::Request::FromCurl->squash_uri(
258             URI->new( 'https://example.com/foo/bar/..' )
259             );
260             # https://example.com/foo/
261              
262             Helper method to clean up relative path elements from the URI the same way
263             that curl does.
264              
265             =cut
266              
267 173     173 1 361821 sub squash_uri( $class, $uri ) {
  173         362  
  173         267  
  173         211  
268 173         713 my $u = $uri->clone;
269 173         1773 my @segments = $u->path_segments;
270              
271 173 100 100     7211 if( $segments[-1] and ($segments[-1] eq '..' or $segments[-1] eq '.' ) ) {
      100        
272 7         17 push @segments, '';
273             };
274              
275 173         334 @segments = grep { $_ ne '.' } @segments;
  369         941  
276              
277             # While we find a pair ( "foo", ".." ) remove that pair
278 173         309 while( grep { $_ eq '..' } @segments ) {
  390         833  
279 11         15 my $i = 0;
280 11         25 while( $i < $#segments ) {
281 31 100 100     97 if( $segments[$i] ne '..' and $segments[$i+1] eq '..') {
282 13         37 splice @segments, $i, 2;
283             } else {
284 18         30 $i++
285             };
286             };
287             };
288              
289 173 100       437 if( @segments < 2 ) {
290 11         21 @segments = ('','');
291             };
292              
293 173         479 $u->path_segments( @segments );
294 173         9759 return $u
295             }
296              
297 560     560   679 sub _add_header( $self, $headers, $h, $value ) {
  560         654  
  560         680  
  560         823  
  560         673  
  560         612  
298 560 100       874 if( exists $headers->{ $h }) {
299 2 50       21 if (!ref( $headers->{ $h })) {
300 2         12 $headers->{ $h } = [ $headers->{ $h }];
301             }
302 2         11 push @{ $headers->{ $h } }, $value;
  2         12  
303             } else {
304 558         1269 $headers->{ $h } = $value;
305             }
306             }
307              
308 13     13   71 sub _maybe_read_data_file( $self, $read_files, $data ) {
  13         34  
  13         43  
  13         33  
  13         25  
309 13         25 my $res;
310 13 100       46 if( $read_files ) {
311 12 100       104 if( $data =~ /^\@(.*)/ ) {
312 6 50       362 open my $fh, '<', $1
313             or die "$1: $!";
314 6         31 local $/; # / for Filter::Simple
315 6         16 binmode $fh;
316 6         201 $res = <$fh>
317             } else {
318 6         17 $res = $data
319             }
320             } else {
321 1 50       7 $res = ($data =~ /^\@(.*)/)
322             ? "... contents of $1 ..."
323             : $data
324             }
325 13         51 return $res
326             }
327              
328 4     4   5 sub _maybe_read_upload_file( $self, $read_files, $data ) {
  4         6  
  4         5  
  4         8  
  4         4  
329 4         5 my $res;
330 4 50       7 if( $read_files ) {
331 4 100       14 if( $data =~ /^<(.*)/ ) {
    100          
332 1 50       120 open my $fh, '<', $1
333             or die "$1: $!";
334 1         11 local $/; # / for Filter::Simple
335 1         6 binmode $fh;
336 1         46 $res = <$fh>
337             } elsif( $data =~ /^\@(.*)/ ) {
338             # Upload the file
339 1         102 $res = [ $1 => basename($1), Content_Type => 'application/octet-stream' ];
340             } else {
341 2         2 $res = $data
342             }
343             } else {
344 0 0       0 if( $data =~ /^[<@](.*)/ ) {
345 0         0 $res = [ undef, basename($1), Content_Type => 'application/octet-stream', Content => "... contents of $1 ..." ],
346             } else {
347 0         0 $res = $data
348             }
349             }
350 4         14 return $res
351             }
352              
353 161     161   302 sub _build_request( $self, $uri, $options, %build_options ) {
  161         297  
  161         233  
  161         235  
  161         375  
  161         247  
354 161         445 my $body;
355 161 100       320 my @headers = @{ $options->{header} || []};
  161         988  
356 161         352 my $method = $options->{request};
357             # Ideally, we shouldn't sort the data but process it in-order
358 161 100       716 my @post_read_data = (@{ $options->{'data'} || []},
359 161 100       291 @{ $options->{'data-ascii'} || [] }
  161         738  
360             );
361             ;
362 161 100       267 my @post_raw_data = @{ $options->{'data-raw'} || [] },
  161         525  
363             ;
364 161 100       218 my @post_urlencode_data = @{ $options->{'data-urlencode'} || [] };
  161         512  
365 161 100       219 my @post_binary_data = @{ $options->{'data-binary'} || [] };
  161         521  
366 161 100       243 my @post_json_data = @{ $options->{'json'} || [] };
  161         679  
367              
368 161         254 my @form_args;
369 161 100       375 if( $options->{form}) {
370             # support --form uploaded_file=@myfile
371             # and --form "uploaded_text=<~/texts/content.txt"
372             push @form_args, map { /^([^=]+)=(.*)$/
373 4 50       43 ? ($1 => $self->_maybe_read_upload_file( $build_options{ read_files }, $2 ))
374 2         5 : () } @{$options->{form}
375 2         4 };
376             };
377 161 100       362 if( $options->{'form-string'}) {
378 2 100       4 push @form_args, map {; /^([^=]+)=(.*)$/ ? ($1 => $2) : (); } @{ $options->{'form-string'}};
  4         22  
  2         4  
379             };
380              
381             # expand the URI here if wanted
382 161         343 my @uris = ($uri);
383 161 100       404 if( ! $options->{ globoff }) {
384 94         1470 @uris = map { $_->{url} } generate_requests( pattern => shift @uris, limit => $build_options{ limit } );
  96         130263  
385             }
386              
387 161 100 100     890 if( $options->{'max-filesize'}
388             and $options->{'max-filesize'} =~ m/^(\d+)([kmg])$/i ) {
389             my $mult = {
390             'k' => 1024,
391             'g' => 1024*1024*1024,
392             'm' => 1024*1024,
393 1         18 }->{ $2 };
394 1         13 $options->{'max-filesize'} = $1 * $mult;
395             }
396              
397 161         259 my @res;
398 161         497 for my $uri (@uris) {
399 163         1683 $uri = URI->new( $uri );
400 163         19257 $uri = $self->squash_uri( $uri );
401              
402 163 100       1299 my $host = $uri->can( 'host_port' ) ? $uri->host_port : "$uri";
403              
404             # Stuff we use unless nothing else hits
405 163         4374 my %request_default_headers = %default_headers;
406              
407             # Sluuuurp
408             # Thous should be hoisted out of the loop
409             @post_binary_data = map {
410 163         460 $self->_maybe_read_data_file( $build_options{ read_files }, $_ );
  3         30  
411             } @post_binary_data;
412              
413             @post_json_data = map {
414 163         292 $self->_maybe_read_data_file( $build_options{ read_files }, $_ );
  1         15  
415             } @post_json_data;
416              
417             @post_read_data = map {
418 163         281 my $v = $self->_maybe_read_data_file( $build_options{ read_files }, $_ );
  8         80  
419 8         43 $v =~ s![\r\n]!!g;
420 8         36 $v
421             } @post_read_data;
422              
423             @post_urlencode_data = map {
424 163 50       301 m/\A([^@=]*)([=@])?(.*)\z/sm
  1         17  
425             or die "This should never happen";
426 1         5 my ($name, $op, $content) = ($1,$2,$3);
427 1 50       16 if(! $op) {
    50          
428 0         0 $content = $name;
429             } elsif( $op eq '@' ) {
430 1         7 $content = "$op$content";
431             };
432 1 50 33     22 if( defined $name and length $name ) {
433 0         0 $name .= '=';
434             } else {
435 1         10 $name = '';
436             };
437 1         15 my $v = $self->_maybe_read_data_file( $build_options{ read_files }, $content );
438 1         6 $name . uri_escape( $v )
439             } @post_urlencode_data;
440              
441 163         422 my $data;
442 163 100 100     1637 if( @post_read_data
    100 100        
      100        
443             or @post_binary_data
444             or @post_raw_data
445             or @post_urlencode_data
446             ) {
447 24         101 $data = join "&",
448             @post_read_data,
449             @post_binary_data,
450             @post_raw_data,
451             @post_urlencode_data
452             ;
453             } elsif( @post_json_data ) {
454 1         5 $data = join '', @post_json_data;
455             }
456              
457 163 100       837 if( @form_args) {
    100          
    100          
    100          
458 4   100     12 $method //= 'POST';
459              
460             #my $req = HTTP::Request::Common::POST(
461             # 'https://example.com',
462             # Content_Type => 'form-data',
463             # Content => \@form_args,
464             #);
465             #$body = $req->content;
466             #$request_default_headers{ 'Content-Type' } = join "; ", $req->headers->content_type;
467              
468             } elsif( $options->{ get }) {
469 2         5 $method = 'GET';
470             # Also, append the POST data to the URL
471 2 50       9 if( $data ) {
472 2         20 my $q = $uri->query;
473 2 100 66     46 if( defined $q and length $q ) {
474 1         8 $q .= "&";
475             } else {
476 1         4 $q = "";
477             };
478 2         4 $q .= $data;
479 2         8 $uri->query( $q );
480             };
481              
482             } elsif( $options->{ head }) {
483 2         12 $method = 'HEAD';
484              
485             } elsif( defined $data ) {
486 23   100     121 $method //= 'POST';
487 23         41 $body = $data;
488              
489 23 100       66 if( @post_json_data ) {
490 1         8 $request_default_headers{ 'Content-Type' } = "application/json";
491 1         5 $request_default_headers{ 'Accept' } = "application/json";
492              
493             } else {
494 22         70 $request_default_headers{ 'Content-Type' } = 'application/x-www-form-urlencoded';
495             };
496              
497             } else {
498 132   100     686 $method ||= 'GET';
499             };
500              
501 163 100       485 if( defined $body ) {
502 23         61 $request_default_headers{ 'Content-Length' } = length $body;
503             };
504              
505 163 100       374 if( $options->{ 'oauth2-bearer' } ) {
506 1         10 push @headers, sprintf 'Authorization: Bearer %s', $options->{'oauth2-bearer'};
507             };
508              
509 163 100       384 if( $options->{ 'user' } ) {
510 5 50 33     127 if( $options->{anyauth}
      33        
      33        
511             || $options->{digest}
512             || $options->{ntlm}
513             || $options->{negotiate}
514             ) {
515             # Nothing to do here, just let LWP::UserAgent do its thing
516             # This means one additional request to fetch the appropriate
517             # 401 response asking for credentials, but ...
518             } else {
519             # $options->{basic} or none at all
520 5         22 my $info = delete $options->{'user'};
521 5 50       41 if( $info !~ /:/ ) {
522             # No password given, so append it
523 0         0 $info .= ':';
524             };
525              
526             # We need to bake this into the header here?!
527 5         87 push @headers, sprintf 'Authorization: Basic %s', encode_base64( $info );
528             }
529             };
530              
531 163         256 my %headers;
532 163         417 for my $kv (
533 43 50       429 (map { /^\s*([^:\s]+)\s*:\s*(.*)$/ ? [$1 => $2] : () } @headers),) {
534 43         171 $self->_add_header( \%headers, @$kv );
535             };
536              
537 163 100       467 if( defined $options->{ 'user-agent' }) {
538 74         298 $self->_add_header( \%headers, "User-Agent", $options->{ 'user-agent' } );
539             };
540              
541 163 50       438 if( defined $options->{ referrer }) {
542 0         0 $self->_add_header( \%headers, "Referer" => $options->{ 'referrer' } );
543             };
544              
545 163 50       367 if( defined $options->{ range }) {
546 0         0 $self->_add_header( \%headers, "Range" => $options->{ 'range' } );
547             };
548              
549             # We want to compare the headers case-insensitively
550 163         485 my %headers_lc = map { lc $_ => 1 } keys %headers;
  115         473  
551              
552 163         449 for my $k (keys %request_default_headers) {
553 372 100       895 if( ! $headers_lc{ lc $k }) {
554 282         755 $self->_add_header( \%headers, $k, $request_default_headers{ $k });
555             };
556             };
557 163 100       421 if( ! $headers{ 'Host' }) {
558 159         374 $self->_add_header( \%headers, 'Host' => $host );
559             };
560              
561 163 100       400 if( defined $options->{ 'cookie-jar' }) {
562 1         10 $options->{'cookie-jar-options'}->{ 'write' } = 1;
563             };
564              
565 163 100       392 if( defined( my $c = $options->{ cookie })) {
566 3 100       22 if( $c =~ /=/ ) {
567 2         7 $headers{ Cookie } = $options->{ 'cookie' };
568             } else {
569 1         9 $options->{'cookie-jar'} = $c;
570 1         4 $options->{'cookie-jar-options'}->{ 'read' } = 1;
571             };
572             };
573              
574             # Curl 7.61.0 ignores these:
575             #if( $options->{ keepalive }) {
576             # $headers{ 'Keep-Alive' } = 1;
577             #} elsif( exists $options->{ keepalive }) {
578             # $headers{ 'Keep-Alive' } = 0;
579             #};
580              
581 163 100       340 if( $options->{ compressed }) {
582 2         22 my $compressions = HTTP::Message::decodable();
583 2         66 $self->_add_header( \%headers, 'Accept-Encoding' => $compressions );
584             };
585              
586 163         219 my $auth;
587 163         287 for my $kind (qw(basic ntlm negotiate)) {
588 489 50       938 if( $options->{$kind}) {
589 0         0 $auth = $kind;
590             }
591             };
592              
593             push @res, HTTP::Request::CurlParameters->new({
594             method => $method,
595             uri => $uri,
596             headers => \%headers,
597             body => $body,
598             maybe auth => $auth,
599             maybe cert => $options->{cert},
600             maybe capath => $options->{capath},
601             maybe credentials => $options->{ user },
602             maybe output => $options->{ output },
603             maybe timeout => $options->{ 'max-time' },
604             maybe cookie_jar => $options->{'cookie-jar'},
605             maybe cookie_jar_options => $options->{'cookie-jar-options'},
606             maybe insecure => $options->{'insecure'},
607             maybe max_filesize => $options->{'max-filesize'},
608             maybe show_error => $options->{'show-error'},
609             maybe fail => $options->{'fail'},
610             maybe unix_socket => $options->{'unix-socket'},
611 163 100       8914 maybe local_address => $options->{'interface'},
612             maybe form_args => scalar @form_args ? \@form_args : undef,
613             });
614             }
615              
616             return @res
617 161         3662 };
618              
619             1;
620              
621             =head1 LIVE DEMO
622              
623             L
624              
625             =head1 KNOWN DIFFERENCES
626              
627             =head2 Incompatible cookie jar formats
628              
629             Until somebody writes a robust Netscape cookie file parser and proper loading
630             and storage for L, this module will not be able to load and
631             save files in the format that Curl uses.
632              
633             =head2 Loading/saving cookie jars is the job of the UA
634              
635             You're expected to instruct your UA to load/save cookie jars:
636              
637             use Path::Tiny;
638             use HTTP::CookieJar::LWP;
639              
640             if( my $cookies = $r->cookie_jar ) {
641             $ua->cookie_jar( HTTP::CookieJar::LWP->new()->load_cookies(
642             path($cookies)->lines
643             ));
644             };
645              
646             =head2 Different Content-Length for POST requests
647              
648             =head2 Different delimiter for form data
649              
650             The delimiter is built by L, and C uses a different
651             mechanism to come up with a unique data delimiter. This results in differences
652             in the raw body content and the C header.
653              
654             =head1 MISSING FUNCTIONALITY
655              
656             =over 4
657              
658             =item *
659              
660             File uploads / content from files
661              
662             While file uploads and reading POST data from files are supported, the content
663             is slurped into memory completely. This can be problematic for large files
664             and little available memory.
665              
666             =item *
667              
668             Mixed data instances
669              
670             Multiple mixed instances of C<--data>, C<--data-ascii>, C<--data-raw>,
671             C<--data-binary> or C<--data-raw> are sorted by type first instead of getting
672             concatenated in the order they appear on the command line.
673             If the order is important to you, use one type only.
674              
675             =item *
676              
677             Multiple sets of parameters from the command line
678              
679             Curl supports the C<< --next >> command line switch which resets
680             parameters for the next URL.
681              
682             This is not (yet) supported.
683              
684             =back
685              
686             =head1 SEE ALSO
687              
688             L
689              
690             L
691              
692             L
693              
694             L - for the inverse function
695              
696             The module HTTP::Request::AsCurl likely also implements a much better version
697             of C<< ->as_curl >> than this module.
698              
699             L - a converter for multiple
700             target languages
701              
702             L
703              
704             =head1 REPOSITORY
705              
706             The public repository of this module is
707             L.
708              
709             =head1 SUPPORT
710              
711             The public support forum of this module is
712             L.
713              
714             =head1 BUG TRACKER
715              
716             Please report bugs in this module via the Github bug queue at
717             L
718              
719             =head1 AUTHOR
720              
721             Max Maischein C
722              
723             =head1 COPYRIGHT (c)
724              
725             Copyright 2018-2026 by Max Maischein C.
726              
727             =head1 LICENSE
728              
729             This module is released under the same terms as Perl itself.
730              
731             =cut