File Coverage

blib/lib/MongoDB/_URI.pm
Criterion Covered Total %
statement 218 296 73.6
branch 112 166 67.4
condition 43 69 62.3
subroutine 32 36 88.8
pod 0 2 0.0
total 405 569 71.1


line stmt bran cond sub pod time code
1             # Copyright 2014 - present MongoDB, Inc.
2             #
3             # Licensed under the Apache License, Version 2.0 (the "License");
4             # you may not use this file except in compliance with the License.
5             # You may obtain a copy of the License at
6             #
7             # http://www.apache.org/licenses/LICENSE-2.0
8             #
9             # Unless required by applicable law or agreed to in writing, software
10             # distributed under the License is distributed on an "AS IS" BASIS,
11             # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12             # See the License for the specific language governing permissions and
13             # limitations under the License.
14              
15 64     64   95282 use strict;
  64         178  
  64         2075  
16 64     64   384 use warnings;
  64         160  
  64         2164  
17             package MongoDB::_URI;
18              
19 64     64   2051 use version;
  64         7621  
  64         480  
20             our $VERSION = 'v2.2.1';
21              
22 64     64   7416 use Moo;
  64         32127  
  64         430  
23 64     64   28496 use MongoDB::Error;
  64         198  
  64         8375  
24 64     64   3005 use Encode ();
  64         42851  
  64         1611  
25 64     64   2482 use Time::HiRes qw(time);
  64         5548  
  64         709  
26 64     64   8905 use MongoDB::_Constants qw( RESCAN_SRV_FREQUENCY_SEC );
  64         163  
  64         3870  
27 64         668 use Types::Standard qw(
28             Any
29             ArrayRef
30             HashRef
31             Str
32             Int
33             Num
34 64     64   465 );
  64         159  
35 64     64   77076 use namespace::clean -except => 'meta';
  64         164  
  64         578  
36 64     64   49709 use Scalar::Util qw/looks_like_number/;
  64         171  
  64         281043  
37              
38             my $uri_re =
39             qr{
40             mongodb(?:\+srv|)://
41             (?: ([^:]*) (?: : ([^@]*) )? @ )? # [username(:password)?@]
42             ([^/?]*) # host1[:port1][,host2[:port2],...[,hostN[:portN]]]
43             (?:
44             / ([^?]*) # /[database]
45             (?: [?] (.*) )? # [?options]
46             )?
47             }x;
48              
49             my %options_with_list_type = map { lc($_) => 1 } qw(
50             readPreferenceTags
51             );
52              
53             has uri => (
54             is => 'ro',
55             isa => Str,
56             required => 1,
57             );
58              
59             has username => (
60             is => 'ro',
61             isa => Any,
62             writer => '_set_username',
63             );
64              
65             has password => (
66             is => 'ro',
67             isa => Any,
68             writer => '_set_password',
69             );
70              
71             has db_name => (
72             is => 'ro',
73             isa => Str,
74             writer => '_set_db_name',
75             default => '',
76             );
77              
78             has options => (
79             is => 'ro',
80             isa => HashRef,
81             writer => '_set_options',
82             default => sub { {} },
83             );
84              
85             has hostids => (
86             is => 'ro',
87             isa => ArrayRef,
88             writer => '_set_hostids',
89             default => sub { [] },
90             );
91              
92             has valid_options => (
93             is => 'lazy',
94             isa => HashRef,
95             );
96              
97             has expires => (
98             is => 'ro',
99             isa => Num,
100             writer => '_set_expires',
101             );
102              
103             sub _build_valid_options {
104 206     206   2102 my $self = shift;
105             return {
106 7210         24627 map { lc($_) => 1 } qw(
107             appName
108             authMechanism
109             authMechanismProperties
110             authSource
111             compressors
112             connect
113             connectTimeoutMS
114             heartbeatFrequencyMS
115             journal
116             localThresholdMS
117             maxStalenessSeconds
118             maxTimeMS
119             readConcernLevel
120             readPreference
121             readPreferenceTags
122             replicaSet
123             serverSelectionTimeoutMS
124             serverSelectionTryOnce
125             socketCheckIntervalMS
126             socketTimeoutMS
127             tlsCAFile
128             tlsCertificateKeyFile
129             tlsCertificateKeyFilePassword
130             w
131             wTimeoutMS
132             zlibCompressionLevel
133 206         509 ), keys %{ $self->_valid_str_to_bool_options }
  206         3349  
134             };
135             }
136              
137             has valid_srv_options => (
138             is => 'lazy',
139             isa => HashRef,
140             );
141              
142             sub _build_valid_srv_options {
143             return {
144 0     0   0 map { lc($_) => 1 } qw(
  0         0  
145             authSource
146             replicaSet
147             )
148             };
149             }
150              
151             has _valid_str_to_bool_options => (
152             is => 'lazy',
153             isa => HashRef,
154             builder => '_build_valid_str_to_bool_options',
155             );
156              
157             sub _build_valid_str_to_bool_options {
158             return {
159 206     206   2002 map { lc($_) => 1 } qw(
  1854         7660  
160             journal
161             retryReads
162             retryWrites
163             serverselectiontryonce
164             ssl
165             tls
166             tlsAllowInvalidCertificates
167             tlsAllowInvalidHostnames
168             tlsInsecure
169             )
170             };
171             }
172              
173             has _extra_options_validation => (
174             is => 'lazy',
175             isa => HashRef,
176             builder => '_build_extra_options_validation',
177             );
178              
179             sub _build_extra_options_validation {
180             return {
181             _PositiveInt => sub {
182 26     26   56 my $v = shift;
183 26 50       94 Int->($v) && $v >= 0;
184             },
185             wtimeoutms => '_PositiveInt',
186             connecttimeoutms => '_PositiveInt',
187             localthresholdms => '_PositiveInt',
188             serverselectiontimeoutms => '_PositiveInt',
189             sockettimeoutms => '_PositiveInt',
190             w => sub {
191 19     19   42 my $v = shift;
192 19 100       97 if (looks_like_number($v)) {
193 15         75 return $v >= 0;
194             }
195 4         13 return 1; # or any string
196             },
197             zlibcompressionlevel => sub {
198 4     4   8 my $v = shift;
199 4 100 66     13 Int->($v) && $v >= -1 && $v <= 9;
200             },
201             heartbeatfrequencyms => sub {
202 4     4   10 my $v = shift;
203 4 50       16 Int->($v) && $v >= 500;
204             },
205             maxstalenessseconds => sub {
206 11     11   21 my $v = shift;
207 11 50 100     47 Int->($v) && ( $v == 1 || $v == -1 || $v >= 90 );
      100        
208             },
209 151     151   8533 };
210             }
211              
212             sub _unescape_all {
213 1485     1485   2513 my $str = shift;
214 1485 50       2837 return '' unless defined $str;
215 1485 100       3531 if ( $str =~ s/%([0-9a-f]{2})/chr(hex($1))/ieg ) {
  156         662  
216 70         241 $str = Encode::decode('UTF-8', $str);
217             }
218 1485         9303 return $str;
219             }
220              
221             sub _parse_doc {
222 9     9   23 my ($name, $string) = @_;
223 9         19 my $set = {};
224 9         30 for my $tag ( split /,/, $string ) {
225 12 50       64 if ( $tag =~ /\S/ ) {
226 12         37 my @kv = map { my $s = $_; $s =~ s{^\s*}{}; $s =~ s{\s*$}{}; $s } split /:/, $tag, 2;
  23         40  
  23         60  
  23         84  
  23         55  
227 12 100       38 if ( @kv != 2 ) {
228 1         9 warn "in option '$name', '$tag' is not a key:value pair\n";
229             return
230 1         7 }
231 11         41 $set->{$kv[0]} = $kv[1];
232             }
233             }
234 8         21 return $set;
235             }
236              
237             sub _parse_options {
238 208     208   5271 my ( $self, $valid, $result, $txt_record ) = @_;
239              
240 208         349 my %parsed;
241 208         778 for my $opt ( split '&', $result->{options} ) {
242 278         793 my @kv = split '=', $opt, -1;
243 278 100       798 MongoDB::UsageError->throw("expected key value pair") unless @kv == 2;
244 272         626 my ( $k, $v ) = map { _unescape_all($_) } @kv;
  544         935  
245             # connection string spec calls for case normalization
246 272         772 ( my $lc_k = $k ) =~ tr[A-Z][a-z];
247 272 100       715 if ( !$valid->{$lc_k} ) {
248 7 50       20 if ( $txt_record ) {
249 0         0 MongoDB::Error->throw("Unsupported option '$k' in URI $self for TXT record $txt_record\n");
250             } else {
251 7         39 warn "Unsupported option '$k' in URI $self\n";
252             }
253 7         55 next;
254             }
255 265 100 100     716 if ( exists $parsed{$lc_k} && !exists $options_with_list_type{$lc_k} ) {
256 2         26 warn "Multiple options were found for the same value '$lc_k'. The first occurrence will be used\n";
257 2         17 next;
258             }
259 263 100       5103 if ( $lc_k eq 'authmechanismproperties' ) {
    100          
    100          
    100          
    100          
    100          
260 3         12 my $temp = _parse_doc( $k, $v );
261 3 50       13 if ( defined $temp ) {
262 3         8 $parsed{$lc_k} = $temp;
263 3 100       14 if ( exists $parsed{$lc_k}{CANONICALIZE_HOST_NAME} ) {
264 2         13 my $temp = __str_to_bool( 'CANONICALIZE_HOST_NAME', $parsed{$lc_k}{CANONICALIZE_HOST_NAME} );
265 2 50       8 if ( defined $temp ) {
266 2         7 $parsed{$lc_k}{CANONICALIZE_HOST_NAME} = $temp;
267             }
268             }
269             }
270             }
271             elsif ( $lc_k eq 'compressors' ) {
272 5         13 my @compressors = split /,/, $v, -1;
273 5         15 my $valid_compressors = {
274             snappy => 1,
275             zlib => 1,
276             zstd => 1
277             };
278 5         11 for my $compressor ( @compressors ) {
279             warn("Unsupported compressor $compressor\n")
280 6 50       16 unless $valid_compressors->{$compressor};
281             }
282 5         22 $parsed{$lc_k} = [ @compressors ];
283             }
284             elsif ( $lc_k eq 'authsource' ) {
285 14         47 $parsed{$lc_k} = $v;
286             }
287             elsif ( $lc_k eq 'readpreferencetags' ) {
288 6   100     31 $parsed{$lc_k} ||= [];
289 6         17 my $temp = _parse_doc( $k, $v );
290 6 100       16 if ( defined $temp ) {
291 5         9 push @{$parsed{$lc_k}}, $temp;
  5         18  
292             }
293             }
294             elsif ( $self->_valid_str_to_bool_options->{ $lc_k } ) {
295 63         542 my $temp = __str_to_bool( $k, $v );
296 63 100       162 if ( defined $temp ) {
297 56         167 $parsed{$lc_k} = $temp
298             }
299             }
300             elsif ( my $opt_validation = $self->_extra_options_validation->{ $lc_k } ) {
301 64 100       1659 unless (ref $opt_validation eq 'CODE') {
302 26         423 $opt_validation = $self->_extra_options_validation->{ $opt_validation };
303             }
304 64         250 my $valid = eval { $opt_validation->($v) };
  64         145  
305 64         10741 my $err = "$@";
306 64 100       2339 if ( ! $valid ) {
307 20         315 warn("Unsupported URI value '$k' = '$v': $err");
308             }
309             else {
310 44         174 $parsed{$lc_k} = $v;
311             }
312             }
313             else {
314 108         2810 $parsed{$lc_k} = $v;
315             }
316             }
317 202 100 100     757 if (
      100        
318             exists $parsed{tlsinsecure}
319             && ( exists $parsed{tlsallowinvalidcertificates}
320             || exists $parsed{tlsallowinvalidhostnames} )
321             )
322             {
323 8         61 MongoDB::Error->throw('tlsInsecure conflicts with other options');
324             }
325             # If both exist, they must be identical.
326 194 100 100     504 if ( exists( $parsed{tls} )
      100        
327             && exists( $parsed{ssl} )
328             && $parsed{tls} != $parsed{ssl} )
329             {
330 4         15 MongoDB::Error->throw('tls and ssl must have the same value');
331             }
332             # If either exists, set them both.
333 190 100       542 if ( exists $parsed{tls} ) {
    100          
334 6         12 $parsed{ssl} = $parsed{tls};
335             }
336             elsif ( exists $parsed{ssl} ) {
337 4         11 $parsed{tls} = $parsed{ssl};
338             }
339 190         575 return \%parsed;
340             }
341              
342             sub _fetch_dns_seedlist {
343 0     0   0 my ( $self, $host_name, $phase ) = @_;
344              
345 0         0 my @split_name = split( '\.', $host_name );
346 0 0       0 MongoDB::Error->throw("URI '$self' must contain domain name and hostname")
347             unless scalar( @split_name ) > 2;
348              
349 0         0 require Net::DNS;
350              
351 0         0 my $res = Net::DNS::Resolver->new;
352 0         0 my $srv_data = $res->query( sprintf( '_mongodb._tcp.%s', $host_name ), 'SRV' );
353              
354 0         0 my @hosts;
355 0         0 my $options = {};
356 0         0 my $domain_name = join( '.', @split_name[1..$#split_name] );
357 0         0 my $minimum_ttl;
358 0 0       0 if ( $srv_data ) {
359 0         0 SRV_RECORD: foreach my $rr ( $srv_data->answer ) {
360 0 0       0 next unless $rr->type eq 'SRV';
361 0         0 my $target = $rr->target;
362             # search for dot before domain name for a valid hostname - can have sub-subdomain
363 0 0       0 unless ( $target =~ /\.\Q$domain_name\E$/ ) {
364 0         0 my $err_msg = "URI '$self' SRV record returns FQDN '$target'"
365 0         0 . " which does not match domain name '${$domain_name}'";
366 0 0 0     0 if ($phase && $phase eq 'init') {
367 0         0 MongoDB::Error->throw($err_msg);
368             }
369             else {
370 0         0 warn $err_msg;
371             }
372 0         0 next SRV_RECORD;
373             }
374 0         0 push @hosts, {
375             target => $target,
376             port => $rr->port,
377             };
378 0 0 0     0 $minimum_ttl = $rr->ttl
379             if not defined $minimum_ttl or $rr->ttl < $minimum_ttl;
380             }
381 0         0 my $txt_data = $res->query( $host_name, 'TXT' );
382 0 0       0 if ( defined $txt_data ) {
383 0         0 my @txt_answers;
384 0         0 foreach my $rr ( $txt_data->answer ) {
385 0 0       0 next unless $rr->type eq 'TXT';
386 0         0 push @txt_answers, $rr;
387             }
388 0 0       0 if ( scalar( @txt_answers ) > 1 ) {
    0          
389 0         0 MongoDB::Error->throw("URI '$self' returned more than one TXT result");
390             } elsif ( scalar( @txt_answers ) == 1 ) {
391 0         0 my $txt_opt_string = join ( '', $txt_answers[0]->txtdata );
392 0         0 $options = $self->_parse_options( $self->valid_srv_options, { options => $txt_opt_string }, $txt_opt_string );
393             }
394             }
395             } else {
396 0         0 MongoDB::Error->throw("URI '$self' does not return any SRV results");
397             }
398              
399 0 0       0 unless (@hosts) {
400 0         0 my $err_msg = "URI '$self' does not return any valid SRV results";
401 0 0 0     0 if ($phase && $phase eq 'init') {
402 0         0 MongoDB::Error->throw($err_msg);
403             }
404             else {
405 0         0 warn $err_msg;
406             }
407             }
408              
409 0 0 0     0 $minimum_ttl = RESCAN_SRV_FREQUENCY_SEC
      0        
410             if $minimum_ttl < RESCAN_SRV_FREQUENCY_SEC
411             && $phase && $phase ne 'init';
412              
413 0         0 return ( \@hosts, $options, time + $minimum_ttl );
414             }
415              
416             sub _parse_srv_uri {
417 5     5   3651 my ( $self, $uri, $phase ) = @_;
418              
419 5         28 my %result;
420              
421 5         229 $uri =~ m{^$uri_re$};
422              
423             (
424             $result{username}, $result{password}, $result{hostids},
425             $result{db_name}, $result{options}
426 5         56 ) = ( $1, $2, $3, $4, $5 );
427              
428 5         18 $result{hostids} = lc _unescape_all( $result{hostids} );
429              
430 5 50 33     37 if ( !defined $result{hostids} || !length $result{hostids} ) {
431 0         0 MongoDB::Error->throw("URI '$self' cannot be empty if using an SRV connection string");
432             }
433              
434 5 100       21 if ( $result{hostids} =~ /,/ ) {
435 1         15 MongoDB::Error->throw("URI '$self' cannot contain a comma or multiple host names if using an SRV connection string");
436             }
437              
438 4 100       18 if ( $result{hostids} =~ /:\d+$/ ) {
439 1         5 MongoDB::Error->throw("URI '$self' cannot contain port number if using an SRV connection string");
440             }
441              
442 3 100       10 if ( defined $result{options} ) {
443 2         51 $result{options} = $self->_parse_options( $self->valid_options, \%result );
444             }
445              
446 3         11 my ( $hosts, $options, $expires ) = $self->_fetch_dns_seedlist( $result{hostids}, $phase );
447              
448             # Default to SSL on unless specified in conn string options
449             $options = {
450             ssl => 'true',
451             %$options,
452 3 100       43 %{ $result{options} || {} },
  3         17  
453             };
454              
455             # Reset str to bool options to string value, as _parse_options changes it to 0/1 if it exists during parsing
456             # means we get the correct value when re-building the uri below.
457 3         7 for my $stb_key ( keys %{ $self->_valid_str_to_bool_options } ) {
  3         60  
458             # use exists just in case
459 27 100       90 next unless exists $options->{ $stb_key };
460 5 50 33     18 $options->{ $stb_key } = ($options->{ $stb_key } || $options->{ $stb_key } eq 'true') ? 'true' : 'false';
461             }
462              
463 3         8 my $auth = "";
464 3 100 66     26 if ( defined $result{username} || defined $result{password} ) {
465 1   100     3 $auth = join(":", map { $_ // "" } $result{username}, $result{password});
  2         9  
466 1         4 $auth .= "@";
467             }
468              
469             my $new_uri = sprintf(
470             'mongodb://%s%s/%s%s%s',
471             $auth,
472 3         35 join( ',', map { sprintf( '%s:%s', $_->{target}, $_->{port} ) } @$hosts ),
473             ($result{db_name} // ""),
474             scalar( keys %$options ) ? '?' : '',
475 3 50 100     8 join( '&', map { sprintf( '%s=%s', $_, __uri_escape( $options->{$_} ) ) } keys %$options ),
  11         27  
476             );
477              
478 3         20 return( $new_uri, $expires );
479             }
480              
481             sub BUILD {
482 562     562 0 22633 my ($self) = @_;
483              
484 562         1622 $self->_initialize_from_uri;
485             }
486              
487             # Options:
488             # - fallback_ttl_sec: Fallback TTL in seconds in case of an error
489             sub check_for_changes {
490 0     0 0 0 my ($self, $options) = @_;
491              
492 0 0 0     0 if (defined $self->{expires} && $self->{expires} <= time) {
493 0         0 my @current = sort @{ $self->{hostids} };
  0         0  
494 0         0 local $@;
495 0         0 my $ok = eval {
496              
497 0         0 $self->_update_from_uri;
498 0         0 1;
499             };
500 0 0       0 if (!$ok) {
501 0         0 warn "Error while fetching SRV records: $@";
502 0         0 $self->{expires} = $options->{fallback_ttl_sec};
503             };
504 0 0       0 return 0
505             unless $ok;
506 0         0 my @new = sort @{ $self->{hostids} };
  0         0  
507 0 0       0 return 1
508             unless @current == @new;
509 0         0 for my $index (0 .. $#current) {
510 0 0       0 return 1
511             unless $new[$index] eq $current[$index];
512             }
513 0         0 return 0;
514             }
515              
516 0         0 return 0;
517             }
518              
519             sub _prepare_dns_hosts {
520 546     546   1184 my ($self, $hostids) = @_;
521              
522 546 100 66     2904 if ( !defined $hostids || !length $hostids ) {
523 5         23 MongoDB::Error->throw("URI '$self' could not be parsed (missing host list)");
524             }
525 541         1959 $hostids = [ map { lc _unescape_all($_) } split ',', $hostids ];
  599         1392  
526 541         1509 for my $hostid (@$hostids) {
527 584 100 66     2260 MongoDB::Error->throw(
528             "URI '$self' could not be parsed (Unix domain sockets are not supported)")
529             if $hostid =~ /\// && $hostid =~ /\.sock/;
530 550 100       1804 MongoDB::Error->throw(
531             "URI '$self' could not be parsed (IP literals are not supported)")
532             if substr( $hostid, 0, 1 ) eq '[';
533 535         1665 my ( $host, $port ) = split ":", $hostid, 2;
534 535 100       1378 MongoDB::Error->throw("host list '@{ $hostids }' contains empty host")
  3         19  
535             unless length $host;
536 532 100       1434 if ( defined $port ) {
537 139 100       763 MongoDB::Error->throw("URI '$self' could not be parsed (invalid port '$port')")
538             unless $port =~ /^\d+$/;
539 130 100 100     1026 MongoDB::Error->throw(
540             "URI '$self' could not be parsed (invalid port '$port' (must be in range [1,65535])")
541             unless $port >= 1 && $port <= 65535;
542             }
543             }
544 475 100       1063 $hostids = [ map { /:/ ? $_ : $_.":27017" } @$hostids ];
  511         2429  
545 475         1426 return $hostids;
546             }
547              
548             sub _update_from_uri {
549 0     0   0 my ($self) = @_;
550              
551 0         0 my $uri = $self->uri;
552 0         0 my %result;
553              
554 0         0 ($uri, my $expires) = $self->_parse_srv_uri( $uri );
555 0         0 $self->{expires} = $expires;
556              
557 0 0       0 if ( $uri !~ m{^$uri_re$} ) {
558 0         0 MongoDB::Error->throw("URI '$self' could not be parsed");
559             }
560              
561 0         0 my $hostids = $3;
562 0         0 $hostids = $self->_prepare_dns_hosts($hostids);
563              
564 0         0 $self->{hostids} = $hostids;
565             }
566              
567             sub _initialize_from_uri {
568 562     562   1354 my ($self) = @_;
569              
570 562         1502 my $uri = $self->uri;
571 562         1118 my %result;
572              
573 562 100       1905 if ( $uri =~ m{^mongodb\+srv://} ) {
574 4         27 ($uri, my $expires) = $self->_parse_srv_uri( $uri, 'init' );
575 2         6 $result{expires} = $expires;
576             }
577              
578             # we throw Error instead of UsageError for errors, to avoid stacktrace revealing credentials
579 560 100       11245 if ( $uri !~ m{^$uri_re$} ) {
580 5         22 MongoDB::Error->throw("URI '$self' could not be parsed");
581             }
582              
583             (
584             $result{username}, $result{password}, $result{hostids},
585             $result{db_name}, $result{options}
586 555         4231 ) = ( $1, $2, $3, $4, $5 );
587              
588 555 100       1820 if ( defined $result{username} ) {
589             MongoDB::Error->throw(
590             "URI '$self' could not be parsed (username must be URL encoded)"
591 72 100       203 ) if __userinfo_invalid_chars($result{username});
592 66         187 $result{username} = _unescape_all( $result{username} );
593             }
594              
595 549 100       1336 if ( defined $result{password} ) {
596             MongoDB::Error->throw(
597             "URI '$self' could not be parsed (password must be URL encoded)"
598 49 100       112 ) if __userinfo_invalid_chars($result{password});
599 46         123 $result{password} = _unescape_all( $result{password} );
600             }
601              
602 546         1522 $result{hostids} = $self->_prepare_dns_hosts($result{hostids});
603              
604 475 100       1261 if ( defined $result{db_name} ) {
605             MongoDB::Error->throw(
606             "URI '$self' could not be parsed (database name must be URL encoded, found unescaped '/'"
607 226 100       622 ) if $result{db_name} =~ /\//;
608 225         472 $result{db_name} = _unescape_all( $result{db_name} );
609             }
610              
611 474 100       1198 if ( defined $result{options} ) {
612 206         4462 $result{options} = $self->_parse_options( $self->valid_options, \%result );
613             }
614              
615 456         1067 for my $attr (qw/username password db_name options hostids expires/) {
616 2736         27014 my $setter = "_set_$attr";
617 2736 100       22889 $self->$setter( $result{$attr} ) if defined $result{$attr};
618             }
619              
620 456         7302 return;
621             }
622              
623             sub __str_to_bool {
624 65     65   146 my ($k, $str) = @_;
625 65 50       151 MongoDB::UsageError->throw("cannot convert undef to bool for key '$k'")
626             unless defined $str;
627 65 100       224 my $ret = $str eq "true" ? 1 : $str eq "false" ? 0 : undef;
    100          
628 65 100       217 warn("expected boolean string 'true' or 'false' for key '$k' but instead received '$str'. Ignoring '$k'.\n")
629             unless defined $ret;
630 65         176 return $ret;
631             }
632              
633             # uri_escape borrowed from HTTP::Tiny 0.070
634             my %escapes = map { chr($_) => sprintf("%%%02X", $_) } 0..255;
635             $escapes{' '}="+";
636             my $unsafe_char = qr/[^A-Za-z0-9\-\._~]/;
637              
638             sub __uri_escape {
639 11     11   18 my ($str) = @_;
640 11 50       29 if ( $] ge '5.008' ) {
641 11         26 utf8::encode($str);
642             }
643             else {
644             $str = pack("U*", unpack("C*", $str)) # UTF-8 encode a byte string
645 64 0   64   686 if ( length $str == do { use bytes; length $str } );
  64         175  
  64         604  
  0         0  
  0         0  
646 0         0 $str = pack("C*", unpack("C*", $str)); # clear UTF-8 flag
647             }
648 11         51 $str =~ s/($unsafe_char)/$escapes{$1}/ge;
  0         0  
649 11         46 return $str;
650             }
651              
652             # Rules for valid userinfo from RFC 3986 Section 3.2.1.
653             my $unreserved = q[a-z0-9._~-]; # use this class last so regex ends in '-'
654             my $subdelimit = q[!$&'()*+,;=];
655             my $allowed = "%$subdelimit$unreserved";
656             my $not_allowed_re = qr/[^$allowed]/i;
657             my $not_pct_enc_re = qr/%(?![0-9a-f]{2})/i;
658              
659             sub __userinfo_invalid_chars {
660 121     121   218 my ($str) = @_;
661 121   100     1004 return $str =~ $not_pct_enc_re || $str =~ $not_allowed_re;
662             }
663              
664             # redact user credentials when stringifying
665             use overload
666             '""' => sub {
667 120     120   13762 (my $s = $_[0]->uri) =~ s{^([^:]+)://[^/]+\@}{$1://[**REDACTED**]\@};
668 120         850 return $s
669             },
670 64     64   19582 'fallback' => 1;
  64         204  
  64         885  
671              
672              
673             1;
674              
675             # vim: ts=4 sts=4 sw=4 et: