File Coverage

blib/lib/MongoDB/_URI.pm
Criterion Covered Total %
statement 192 290 66.2
branch 98 160 61.2
condition 35 66 53.0
subroutine 31 36 86.1
pod 0 2 0.0
total 356 554 64.2


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