File Coverage

lib/Pcore/Util/URI/Host.pm
Criterion Covered Total %
statement 98 223 43.9
branch 34 86 39.5
condition 12 23 52.1
subroutine 16 31 51.6
pod 0 4 0.0
total 160 367 43.6


line stmt bran cond sub pod time code
1             package Pcore::Util::URI::Host;
2              
3 4     4   594 use Pcore -class;
  4         11  
  4         27  
4 4     4   41 use Pcore::Util::Text qw[decode_utf8 encode_utf8];
  4         11  
  4         36  
5 4     4   32 use AnyEvent::Socket qw[];
  4         12  
  4         97  
6 4     4   1600 use Pcore::Util::IDN qw[:ALL];
  4         13  
  4         30  
7              
8             use overload #
9             q[""] => sub {
10 0     0   0 return $_[0]->{name};
11             },
12             q[cmp] => sub {
13 21 50   21   207 return !$_[2] ? $_[0]->{name} cmp $_[1] : $_[1] cmp $_[0]->{name};
14             },
15 4     4   33 fallback => undef;
  4         9  
  4         53  
16              
17             has name => ( is => 'ro', required => 1 );
18             has name_utf8 => ( is => 'lazy', init_arg => undef );
19             has is_ip => ( is => 'lazy', init_arg => undef );
20             has is_ipv4 => ( is => 'lazy', init_arg => undef );
21             has is_ipv6 => ( is => 'lazy', init_arg => undef );
22             has is_domain => ( is => 'lazy', init_arg => undef );
23             has is_valid => ( is => 'lazy', init_arg => undef );
24             has is_tld => ( is => 'lazy', init_arg => undef ); # domain is a known TLD
25             has tld => ( is => 'lazy', init_arg => undef );
26             has tld_utf8 => ( is => 'lazy', init_arg => undef );
27             has tld_is_valid => ( is => 'lazy', init_arg => undef );
28             has canon => ( is => 'lazy', init_arg => undef ); # host without www. prefix
29             has canon_utf8 => ( is => 'lazy', init_arg => undef );
30             has is_pub_suffix => ( is => 'lazy', init_arg => undef ); # domain is a pub. suffix
31             has is_pub_suffix_parent => ( is => 'lazy', init_arg => undef );
32             has pub_suffix => ( is => 'lazy', init_arg => undef );
33             has pub_suffix_utf8 => ( is => 'lazy', init_arg => undef );
34             has is_root_domain => ( is => 'lazy', init_arg => undef ); # domain is a root domain
35             has root_domain => ( is => 'lazy', init_arg => undef );
36             has root_domain_utf8 => ( is => 'lazy', init_arg => undef );
37             has root_label => ( is => 'lazy', init_arg => undef );
38             has root_label_utf8 => ( is => 'lazy', init_arg => undef );
39              
40             our $TLD;
41             our $PUB_SUFFIX;
42              
43             # NOTE host should be in UTF-8 or ASCII punycoded, UTF-8 encoded - is invalid value
44             around new => sub ( $orig, $self, $host ) {
45              
46             # removing double "."
47             $host =~ s/[.]+/./smg if index( $host, q[..] ) != -1;
48              
49             # removing leading "."
50             substr $host, 0, 1, q[] if substr( $host, 0, 1 ) eq q[.];
51              
52             # removing trailing "."
53             substr $host, -1, 1, q[] if substr( $host, -1, 1 ) eq q[.];
54              
55             $host = domain_to_ascii($host) if utf8::is_utf8($host);
56              
57             return bless { name => lc $host }, __PACKAGE__;
58             };
59              
60 0     0 0 0 sub update_all ( $self ) {
  0         0  
  0         0  
61              
62             # update TLD
63 0         0 print 'updating tld.dat ... ';
64              
65 0 0       0 if ( my $res = P->http->get( 'https://data.iana.org/TLD/tlds-alpha-by-domain.txt', buf_size => 0, on_progress => 0 ) ) {
66 0         0 my $domains;
67              
68 0 0       0 for my $domain_ascii ( map {lc} grep { $_ && !/\A\s*#/sm } split /\n/sm, $res->body->$* ) {
  0         0  
  0         0  
69 0         0 $domains->{$domain_ascii} = domain_to_utf8($domain_ascii);
70             }
71              
72 0         0 $ENV->share->store( '/data/tld.dat', \encode_utf8( join $LF, map {"$domains->{$_};$_"} sort { $domains->{$a} cmp $domains->{$b} } keys $domains->%* ), 'Pcore' );
  0         0  
  0         0  
73              
74 0         0 undef $TLD;
75              
76 0         0 say 'done';
77             }
78             else {
79 0         0 say 'error';
80              
81 0         0 return 0;
82             }
83              
84             # update pub. suffixes, should be updated after TLDs
85 0         0 print 'updating pub_suffix.dat ... ';
86              
87 0 0       0 if ( my $res = P->http->get( 'https://publicsuffix.org/list/effective_tld_names.dat', buf_size => 0, on_progress => 0 ) ) {
88 0         0 my $suffixes = {};
89              
90 0         0 decode_utf8 $res->body->$*;
91              
92 0         0 for my $domain_utf8 ( split /\n/sm, $res->body->$* ) {
93              
94             # remove spaces
95 0         0 $domain_utf8 =~ s/\s//smg;
96              
97             # remove comments
98 0         0 $domain_utf8 =~ s[//.*][]sm;
99              
100             # ignore empty lines
101 0 0       0 next if $domain_utf8 eq q[];
102              
103 0         0 $suffixes->{ domain_to_ascii( lc $domain_utf8 ) } = lc $domain_utf8;
104             }
105              
106             # add tlds
107 0         0 $suffixes->@{ keys $self->tlds->%* } = values $self->tlds->%*;
108              
109             # add pub. suffix parent as pub. suffix
110 0         0 for my $domain ( keys $suffixes->%* ) {
111 0         0 my @labels = split /[.]/sm, $domain;
112              
113             # remove left label
114 0         0 shift @labels;
115              
116 0         0 while (@labels) {
117 0         0 my $label = shift @labels;
118              
119             # ignore "*" label
120 0 0       0 next if $label eq q[*];
121              
122 0         0 my $parent_ascii = join q[.], $label, @labels;
123              
124 0 0       0 $suffixes->{$parent_ascii} = domain_to_utf8($parent_ascii) if !exists $suffixes->{$parent_ascii};
125             }
126             }
127              
128 0         0 $ENV->share->store( '/data/pub_suffix.dat', \encode_utf8( join $LF, map {"$suffixes->{$_};$_"} sort { $suffixes->{$a} cmp $suffixes->{$b} } keys $suffixes->%* ), 'Pcore' );
  0         0  
  0         0  
129              
130 0         0 undef $PUB_SUFFIX;
131              
132 0         0 say 'done';
133             }
134             else {
135 0         0 say 'error';
136              
137 0         0 return 0;
138             }
139              
140 0         0 return 1;
141             }
142              
143 0     0 0 0 sub tlds ( $self ) {
  0         0  
  0         0  
144 0   0     0 $TLD //= do {
145 0         0 my $tlds;
146              
147 0         0 for my $rec ( split /\n/sm, P->file->read_text( $ENV->share->get('/data/tld.dat') )->$* ) {
148 0         0 my ( $utf8, $ascii ) = split /;/sm, $rec;
149              
150 0         0 $tlds->{$ascii} = $utf8;
151             }
152              
153 0         0 $tlds;
154             };
155              
156 0         0 return $TLD;
157             }
158              
159 65     65 0 469 sub pub_suffixes ( $self ) {
  65         80  
  65         74  
160 65   66     138 $PUB_SUFFIX //= do {
161 2         4 my $pub_suffix;
162              
163 2         50 for my $rec ( split /\n/sm, P->file->read_text( $ENV->share->get('/data/pub_suffix.dat') )->$* ) {
164 16916         42818 my ( $utf8, $ascii ) = split /;/sm, $rec;
165              
166 16916         49733 $pub_suffix->{$ascii} = $utf8;
167             }
168              
169 2         1123 $pub_suffix;
170             };
171              
172 65         112 return $PUB_SUFFIX;
173             }
174              
175 0     0 0 0 sub to_string ($self) {
  0         0  
  0         0  
176 0         0 return $self->{name};
177             }
178              
179 0     0   0 sub _build_name_utf8 ($self) {
  0         0  
  0         0  
180 0         0 return domain_to_utf8( $self->name );
181             }
182              
183 90     90   723 sub _build_is_ip ($self) {
  90         127  
  90         108  
184 90 50 33     1636 if ( $self->name && ( $self->is_ipv4 || $self->is_ipv6 ) ) {
      33        
185 0         0 return 1;
186             }
187             else {
188 90         366 return 0;
189             }
190             }
191              
192 101     101   940 sub _build_is_ipv4 ($self) {
  101         127  
  101         122  
193 101 100 66     435 if ( $self->name && AnyEvent::Socket::parse_ipv4( $self->name ) ) {
194 9         254 return 1;
195             }
196             else {
197 92         2255 return 0;
198             }
199             }
200              
201 93     93   774 sub _build_is_ipv6 ($self) {
  93         125  
  93         118  
202 93 100 66     379 if ( $self->name && AnyEvent::Socket::parse_ipv6( $self->name ) ) {
203 3         208 return 1;
204             }
205             else {
206 90         851 return 0;
207             }
208             }
209              
210 64     64   469 sub _build_is_domain ($self) {
  64         94  
  64         70  
211 64 50       187 return 0 if $self->name eq q[];
212              
213 64 50       1010 return $self->is_ip ? 0 : 1;
214             }
215              
216 0     0   0 sub _build_is_valid ($self) {
  0         0  
  0         0  
217 0 0       0 return 1 if $self->is_ip;
218              
219 0 0       0 if ( my $name = $self->name ) {
220 0 0       0 return 0 if bytes::length($name) > 255; # max length is 255 octets
221              
222 0 0       0 return 0 if $name =~ /[^[:alnum:]._-]/sm; # allowed chars
223              
224 0 0       0 return 0 if $name !~ /\A[[:alnum:]]/sm; # first character should be letter or digit
225              
226 0 0       0 return 0 if $name !~ /[[:alnum:]]\z/sm; # last character should be letter or digit
227              
228 0         0 for ( split /[.]/sm, $name ) {
229 0 0       0 return 0 if bytes::length($_) > 63; # max. label length is 63 octets
230             }
231              
232 0         0 return 1;
233             }
234              
235             # host considered invalid if host is empty
236 0         0 return 0;
237             }
238              
239 0     0   0 sub _build_is_tld ($self) {
  0         0  
  0         0  
240 0 0       0 return 0 unless $self->is_domain;
241              
242 0 0       0 return $self->tld eq $self->name ? 1 : 0;
243             }
244              
245 0     0   0 sub _build_tld ($self) {
  0         0  
  0         0  
246 0 0       0 if ( $self->is_ip ) {
247 0         0 return q[];
248             }
249             else {
250 0         0 return substr $self->name, rindex( $self->name, q[.] ) + 1;
251             }
252             }
253              
254 0     0   0 sub _build_tld_utf8 ($self) {
  0         0  
  0         0  
255 0         0 return domain_to_utf8( $self->tld );
256             }
257              
258 0     0   0 sub _build_tld_is_valid ($self) {
  0         0  
  0         0  
259 0 0       0 return exists $self->tlds->{ $self->tld } ? 1 : 0;
260             }
261              
262 22     22   192 sub _build_canon ($self) {
  22         30  
  22         28  
263 22         44 my $name = $self->name;
264              
265 22 100 66     104 substr $name, 0, 4, q[] if $name && index( $name, 'www.' ) == 0;
266              
267 22         173 return $name;
268             }
269              
270 0     0   0 sub _build_canon_utf8 ($self) {
  0         0  
  0         0  
271 0         0 return domain_to_utf8( $self->canon );
272             }
273              
274 39     39   320 sub _build_is_pub_suffix ($self) {
  39         52  
  39         50  
275 39 50       685 return 0 unless $self->is_domain;
276              
277 39 100       787 return length( $self->pub_suffix ) == length( $self->name ) ? 1 : 0;
278             }
279              
280             # A public suffix is a set of DNS names or wildcards concatenated with dots.
281             # It represents the part of a domain name which is not under the control of the individual registrant.
282             # TODO wildcards like *.*.foo.bar should be supported
283 64     64   760 sub _build_pub_suffix ($self) {
  64         79  
  64         82  
284 64 50       852 return q[] unless $self->is_domain;
285              
286 64         385 my $pub_suffixes = $self->pub_suffixes;
287              
288 64         95 my $pub_suffix;
289              
290 64 50       181 if ( my $name = $self->name ) {
291 64 100       153 if ( exists $pub_suffixes->{$name} ) {
292 14         27 $pub_suffix = $name;
293             }
294             else {
295 50         167 my @labels = split /[.]/sm, $name;
296              
297 50 50       111 if ( @labels > 1 ) {
298 50         256 while (@labels) {
299 72         128 my $first_label = shift @labels;
300              
301 72         166 my $parent = join q[.], @labels;
302              
303 72 100       185 if ( exists $pub_suffixes->{"*.$parent"} ) {
304 19         39 my $subdomain = "$first_label.$parent";
305              
306 19 100       58 if ( !exists $pub_suffixes->{"!$subdomain"} ) {
307 14         21 $pub_suffix = $subdomain;
308              
309 14         31 last;
310             }
311             }
312              
313 58 100       134 if ( exists $pub_suffixes->{$parent} ) {
314 34         63 $pub_suffix = $parent;
315              
316 34         66 last;
317             }
318              
319 24 100       68 last if @labels == 1;
320             }
321             }
322             }
323             }
324              
325 64   100     468 return $pub_suffix // q[];
326             }
327              
328 0     0   0 sub _build_pub_suffix_utf8 ($self) {
  0         0  
  0         0  
329 0 0       0 if ( my $pub_suffix = $self->pub_suffix ) {
330 0         0 return domain_to_utf8($pub_suffix);
331             }
332             else {
333 0         0 return q[];
334             }
335             }
336              
337 2     2   79 sub _build_is_root_domain ($self) {
  2         6  
  2         5  
338 2 50       48 return 0 unless $self->is_domain;
339              
340 2 100       36 return length( $self->root_domain ) eq length( $self->name ) ? 1 : 0;
341             }
342              
343 5     5   89 sub _build_root_domain ($self) {
  5         10  
  5         6  
344 5 50       70 return q[] unless $self->is_domain;
345              
346 5 100       95 if ( my $pub_suffix = $self->pub_suffix ) {
347 4         93 my $canon = $self->canon;
348              
349 4 100       16 return q[] if length $pub_suffix >= length $canon;
350              
351 3         10 my $root = substr $canon, 0, length($canon) - length($pub_suffix) - 1;
352              
353 3         158 return ( split /[.]/sm, $root )[-1] . ".$pub_suffix";
354             }
355              
356 1         6 return q[];
357             }
358              
359 0     0     sub _build_root_domain_utf8 ($self) {
  0            
  0            
360 0 0         if ( my $root_domain = $self->root_domain ) {
361 0           return domain_to_utf8($root_domain);
362             }
363             else {
364 0           return q[];
365             }
366              
367             }
368              
369 0     0     sub _build_root_label ($self) {
  0            
  0            
370 0 0         if ( my $root_domain = $self->root_domain ) {
371 0           $root_domain =~ s/[.].+\z//sm;
372              
373 0           return $root_domain;
374             }
375              
376 0           return q[];
377             }
378              
379 0     0     sub _build_root_label_utf8 ($self) {
  0            
  0            
380 0 0         if ( my $root_domain_utf8 = $self->root_domain_utf8 ) {
381 0           $root_domain_utf8 =~ s/[.].+\z//sm;
382              
383 0           return $root_domain_utf8;
384             }
385              
386 0           return q[];
387             }
388              
389             1;
390             ## -----SOURCE FILTER LOG BEGIN-----
391             ##
392             ## PerlCritic profile "pcore-script" policy violations:
393             ## +------+----------------------+----------------------------------------------------------------------------------------------------------------+
394             ## | Sev. | Lines | Policy |
395             ## |======+======================+================================================================================================================|
396             ## | 3 | 306 | ControlStructures::ProhibitDeepNests - Code structure is deeply nested |
397             ## +------+----------------------+----------------------------------------------------------------------------------------------------------------+
398             ##
399             ## -----SOURCE FILTER LOG END-----
400             __END__
401             =pod
402              
403             =encoding utf8
404              
405             =head1 NAME
406              
407             Pcore::Util::URI::Host
408              
409             =head1 SYNOPSIS
410              
411             =head1 DESCRIPTION
412              
413             =cut