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   79853 use strict;
  64         151  
  64         1873  
16 64     64   305 use warnings;
  64         141  
  64         1920  
17             package MongoDB::_URI;
18              
19 64     64   1729 use version;
  64         6403  
  64         440  
20             our $VERSION = 'v2.2.2';
21              
22 64     64   6062 use Moo;
  64         26624  
  64         399  
23 64     64   24514 use MongoDB::Error;
  64         184  
  64         7170  
24 64     64   2558 use Encode ();
  64         34209  
  64         1454  
25 64     64   2047 use Time::HiRes qw(time);
  64         4599  
  64         588  
26 64     64   7989 use MongoDB::_Constants qw( RESCAN_SRV_FREQUENCY_SEC );
  64         179  
  64         3391  
27 64         559 use Types::Standard qw(
28             Any
29             ArrayRef
30             HashRef
31             Str
32             Int
33             Num
34 64     64   399 );
  64         123  
35 64     64   66749 use namespace::clean -except => 'meta';
  64         140  
  64         535  
36 64     64   43130 use Scalar::Util qw/looks_like_number/;
  64         152  
  64         243737  
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   2266 my $self = shift;
105             return {
106 7210         22276 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         579 ), keys %{ $self->_valid_str_to_bool_options }
  206         3367  
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         7057  
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   40 my $v = shift;
183 26 50       89 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   31 my $v = shift;
192 19 100       78 if (looks_like_number($v)) {
193 15         43 return $v >= 0;
194             }
195 4         10 return 1; # or any string
196             },
197             zlibcompressionlevel => sub {
198 4     4   6 my $v = shift;
199 4 100 66     16 Int->($v) && $v >= -1 && $v <= 9;
200             },
201             heartbeatfrequencyms => sub {
202 4     4   9 my $v = shift;
203 4 50       14 Int->($v) && $v >= 500;
204             },
205             maxstalenessseconds => sub {
206 11     11   25 my $v = shift;
207 11 50 100     133 Int->($v) && ( $v == 1 || $v == -1 || $v >= 90 );
      100        
208             },
209 151     151   8382 };
210             }
211              
212             sub _unescape_all {
213 1485     1485   2338 my $str = shift;
214 1485 50       2576 return '' unless defined $str;
215 1485 100       3316 if ( $str =~ s/%([0-9a-f]{2})/chr(hex($1))/ieg ) {
  156         585  
216 70         228 $str = Encode::decode('UTF-8', $str);
217             }
218 1485         8288 return $str;
219             }
220              
221             sub _parse_doc {
222 9     9   24 my ($name, $string) = @_;
223 9         14 my $set = {};
224 9         25 for my $tag ( split /,/, $string ) {
225 12 50       44 if ( $tag =~ /\S/ ) {
226 12         29 my @kv = map { my $s = $_; $s =~ s{^\s*}{}; $s =~ s{\s*$}{}; $s } split /:/, $tag, 2;
  23         34  
  23         79  
  23         119  
  23         54  
227 12 100       35 if ( @kv != 2 ) {
228 1         8 warn "in option '$name', '$tag' is not a key:value pair\n";
229             return
230 1         7 }
231 11         34 $set->{$kv[0]} = $kv[1];
232             }
233             }
234 8         18 return $set;
235             }
236              
237             sub _parse_options {
238 208     208   5155 my ( $self, $valid, $result, $txt_record ) = @_;
239              
240 208         371 my %parsed;
241 208         817 for my $opt ( split '&', $result->{options} ) {
242 278         836 my @kv = split '=', $opt, -1;
243 278 100       868 MongoDB::UsageError->throw("expected key value pair") unless @kv == 2;
244 272         603 my ( $k, $v ) = map { _unescape_all($_) } @kv;
  544         1058  
245             # connection string spec calls for case normalization
246 272         655 ( my $lc_k = $k ) =~ tr[A-Z][a-z];
247 272 100       716 if ( !$valid->{$lc_k} ) {
248 7 50       17 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         34 warn "Unsupported option '$k' in URI $self\n";
252             }
253 7         59 next;
254             }
255 265 100 100     697 if ( exists $parsed{$lc_k} && !exists $options_with_list_type{$lc_k} ) {
256 2         24 warn "Multiple options were found for the same value '$lc_k'. The first occurrence will be used\n";
257 2         20 next;
258             }
259 263 100       4659 if ( $lc_k eq 'authmechanismproperties' ) {
    100          
    100          
    100          
    100          
    100          
260 3         14 my $temp = _parse_doc( $k, $v );
261 3 50       12 if ( defined $temp ) {
262 3         7 $parsed{$lc_k} = $temp;
263 3 100       14 if ( exists $parsed{$lc_k}{CANONICALIZE_HOST_NAME} ) {
264 2         10 my $temp = __str_to_bool( 'CANONICALIZE_HOST_NAME', $parsed{$lc_k}{CANONICALIZE_HOST_NAME} );
265 2 50       7 if ( defined $temp ) {
266 2         8 $parsed{$lc_k}{CANONICALIZE_HOST_NAME} = $temp;
267             }
268             }
269             }
270             }
271             elsif ( $lc_k eq 'compressors' ) {
272 5         10 my @compressors = split /,/, $v, -1;
273 5         14 my $valid_compressors = {
274             snappy => 1,
275             zlib => 1,
276             zstd => 1
277             };
278 5         9 for my $compressor ( @compressors ) {
279             warn("Unsupported compressor $compressor\n")
280 6 50       13 unless $valid_compressors->{$compressor};
281             }
282 5         18 $parsed{$lc_k} = [ @compressors ];
283             }
284             elsif ( $lc_k eq 'authsource' ) {
285 14         73 $parsed{$lc_k} = $v;
286             }
287             elsif ( $lc_k eq 'readpreferencetags' ) {
288 6   100     25 $parsed{$lc_k} ||= [];
289 6         14 my $temp = _parse_doc( $k, $v );
290 6 100       14 if ( defined $temp ) {
291 5         7 push @{$parsed{$lc_k}}, $temp;
  5         13  
292             }
293             }
294             elsif ( $self->_valid_str_to_bool_options->{ $lc_k } ) {
295 63         450 my $temp = __str_to_bool( $k, $v );
296 63 100       125 if ( defined $temp ) {
297 56         142 $parsed{$lc_k} = $temp
298             }
299             }
300             elsif ( my $opt_validation = $self->_extra_options_validation->{ $lc_k } ) {
301 64 100       1370 unless (ref $opt_validation eq 'CODE') {
302 26         368 $opt_validation = $self->_extra_options_validation->{ $opt_validation };
303             }
304 64         213 my $valid = eval { $opt_validation->($v) };
  64         127  
305 64         9173 my $err = "$@";
306 64 100       1942 if ( ! $valid ) {
307 20         251 warn("Unsupported URI value '$k' = '$v': $err");
308             }
309             else {
310 44         143 $parsed{$lc_k} = $v;
311             }
312             }
313             else {
314 108         2978 $parsed{$lc_k} = $v;
315             }
316             }
317 202 100 100     952 if (
      100        
318             exists $parsed{tlsinsecure}
319             && ( exists $parsed{tlsallowinvalidcertificates}
320             || exists $parsed{tlsallowinvalidhostnames} )
321             )
322             {
323 8         77 MongoDB::Error->throw('tlsInsecure conflicts with other options');
324             }
325             # If both exist, they must be identical.
326 194 100 100     620 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       683 if ( exists $parsed{tls} ) {
    100          
334 6         10 $parsed{ssl} = $parsed{tls};
335             }
336             elsif ( exists $parsed{ssl} ) {
337 4         7 $parsed{tls} = $parsed{ssl};
338             }
339 190         578 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   3305 my ( $self, $uri, $phase ) = @_;
418              
419 5         7 my %result;
420              
421 5         163 $uri =~ m{^$uri_re$};
422              
423             (
424             $result{username}, $result{password}, $result{hostids},
425             $result{db_name}, $result{options}
426 5         46 ) = ( $1, $2, $3, $4, $5 );
427              
428 5         16 $result{hostids} = lc _unescape_all( $result{hostids} );
429              
430 5 50 33     28 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       20 if ( $result{hostids} =~ /,/ ) {
435 1         4 MongoDB::Error->throw("URI '$self' cannot contain a comma or multiple host names if using an SRV connection string");
436             }
437              
438 4 100       13 if ( $result{hostids} =~ /:\d+$/ ) {
439 1         3 MongoDB::Error->throw("URI '$self' cannot contain port number if using an SRV connection string");
440             }
441              
442 3 100       8 if ( defined $result{options} ) {
443 2         41 $result{options} = $self->_parse_options( $self->valid_options, \%result );
444             }
445              
446 3         9 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       37 %{ $result{options} || {} },
  3         15  
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         5 for my $stb_key ( keys %{ $self->_valid_str_to_bool_options } ) {
  3         49  
458             # use exists just in case
459 27 100       77 next unless exists $options->{ $stb_key };
460 5 50 33     13 $options->{ $stb_key } = ($options->{ $stb_key } || $options->{ $stb_key } eq 'true') ? 'true' : 'false';
461             }
462              
463 3         5 my $auth = "";
464 3 100 66     31 if ( defined $result{username} || defined $result{password} ) {
465 1   100     3 $auth = join(":", map { $_ // "" } $result{username}, $result{password});
  2         8  
466 1         3 $auth .= "@";
467             }
468              
469             my $new_uri = sprintf(
470             'mongodb://%s%s/%s%s%s',
471             $auth,
472 3         38 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         21  
476             );
477              
478 3         16 return( $new_uri, $expires );
479             }
480              
481             sub BUILD {
482 562     562 0 21160 my ($self) = @_;
483              
484 562         1765 $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   1198 my ($self, $hostids) = @_;
521              
522 546 100 66     2817 if ( !defined $hostids || !length $hostids ) {
523 5         19 MongoDB::Error->throw("URI '$self' could not be parsed (missing host list)");
524             }
525 541         1750 $hostids = [ map { lc _unescape_all($_) } split ',', $hostids ];
  599         1370  
526 541         1619 for my $hostid (@$hostids) {
527 584 100 66     2218 MongoDB::Error->throw(
528             "URI '$self' could not be parsed (Unix domain sockets are not supported)")
529             if $hostid =~ /\// && $hostid =~ /\.sock/;
530 550 100       1736 MongoDB::Error->throw(
531             "URI '$self' could not be parsed (IP literals are not supported)")
532             if substr( $hostid, 0, 1 ) eq '[';
533 535         1578 my ( $host, $port ) = split ":", $hostid, 2;
534 535 100       1361 MongoDB::Error->throw("host list '@{ $hostids }' contains empty host")
  3         20  
535             unless length $host;
536 532 100       1325 if ( defined $port ) {
537 139 100       656 MongoDB::Error->throw("URI '$self' could not be parsed (invalid port '$port')")
538             unless $port =~ /^\d+$/;
539 130 100 100     849 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       879 $hostids = [ map { /:/ ? $_ : $_.":27017" } @$hostids ];
  511         2159  
545 475         1362 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   1230 my ($self) = @_;
569              
570 562         1628 my $uri = $self->uri;
571 562         1020 my %result;
572              
573 562 100       1856 if ( $uri =~ m{^mongodb\+srv://} ) {
574 4         17 ($uri, my $expires) = $self->_parse_srv_uri( $uri, 'init' );
575 2         5 $result{expires} = $expires;
576             }
577              
578             # we throw Error instead of UsageError for errors, to avoid stacktrace revealing credentials
579 560 100       10806 if ( $uri !~ m{^$uri_re$} ) {
580 5         19 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         3974 ) = ( $1, $2, $3, $4, $5 );
587              
588 555 100       1651 if ( defined $result{username} ) {
589             MongoDB::Error->throw(
590             "URI '$self' could not be parsed (username must be URL encoded)"
591 72 100       253 ) if __userinfo_invalid_chars($result{username});
592 66         220 $result{username} = _unescape_all( $result{username} );
593             }
594              
595 549 100       1423 if ( defined $result{password} ) {
596             MongoDB::Error->throw(
597             "URI '$self' could not be parsed (password must be URL encoded)"
598 49 100       105 ) if __userinfo_invalid_chars($result{password});
599 46         128 $result{password} = _unescape_all( $result{password} );
600             }
601              
602 546         1634 $result{hostids} = $self->_prepare_dns_hosts($result{hostids});
603              
604 475 100       1235 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       621 ) if $result{db_name} =~ /\//;
608 225         474 $result{db_name} = _unescape_all( $result{db_name} );
609             }
610              
611 474 100       1154 if ( defined $result{options} ) {
612 206         4139 $result{options} = $self->_parse_options( $self->valid_options, \%result );
613             }
614              
615 456         1034 for my $attr (qw/username password db_name options hostids expires/) {
616 2736         26226 my $setter = "_set_$attr";
617 2736 100       20964 $self->$setter( $result{$attr} ) if defined $result{$attr};
618             }
619              
620 456         6889 return;
621             }
622              
623             sub __str_to_bool {
624 65     65   129 my ($k, $str) = @_;
625 65 50       124 MongoDB::UsageError->throw("cannot convert undef to bool for key '$k'")
626             unless defined $str;
627 65 100       153 my $ret = $str eq "true" ? 1 : $str eq "false" ? 0 : undef;
    100          
628 65 100       176 warn("expected boolean string 'true' or 'false' for key '$k' but instead received '$str'. Ignoring '$k'.\n")
629             unless defined $ret;
630 65         134 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   15 my ($str) = @_;
640 11 50       18 if ( $] ge '5.008' ) {
641 11         16 utf8::encode($str);
642             }
643             else {
644             $str = pack("U*", unpack("C*", $str)) # UTF-8 encode a byte string
645 64 0   64   610 if ( length $str == do { use bytes; length $str } );
  64         449  
  64         551  
  0         0  
  0         0  
646 0         0 $str = pack("C*", unpack("C*", $str)); # clear UTF-8 flag
647             }
648 11         39 $str =~ s/($unsafe_char)/$escapes{$1}/ge;
  0         0  
649 11         36 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   251 my ($str) = @_;
661 121   100     1140 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   11969 (my $s = $_[0]->uri) =~ s{^([^:]+)://[^/]+\@}{$1://[**REDACTED**]\@};
668 120         658 return $s
669             },
670 64     64   16836 'fallback' => 1;
  64         189  
  64         741  
671              
672              
673             1;
674              
675             # vim: ts=4 sts=4 sw=4 et: