File Coverage

blib/lib/Net/DNS/Domain.pm
Criterion Covered Total %
statement 95 95 100.0
branch 26 26 100.0
condition 7 7 100.0
subroutine 20 20 100.0
pod 7 7 100.0
total 155 155 100.0


line stmt bran cond sub pod time code
1             package Net::DNS::Domain;
2              
3 98     98   1604 use strict;
  98         202  
  98         2863  
4 98     98   472 use warnings;
  98         174  
  98         4498  
5              
6             our $VERSION = (qw$Id: Domain.pm 1913 2023-04-20 12:33:30Z willem $)[2];
7              
8              
9             =head1 NAME
10              
11             Net::DNS::Domain - DNS domains
12              
13             =head1 SYNOPSIS
14              
15             use Net::DNS::Domain;
16              
17             $domain = Net::DNS::Domain->new('example.com');
18             $name = $domain->name;
19              
20             =head1 DESCRIPTION
21              
22             The Net::DNS::Domain module implements a class of abstract DNS
23             domain objects with associated class and instance methods.
24              
25             Each domain object instance represents a single DNS domain which
26             has a fixed identity throughout its lifetime.
27              
28             Internally, the primary representation is a (possibly empty) list
29             of ASCII domain name labels, and optional link to an origin domain
30             object topologically closer to the DNS root.
31              
32             The computational expense of Unicode character-set conversion is
33             partially mitigated by use of caches.
34              
35             =cut
36              
37              
38 98     98   1551 use integer;
  98         210  
  98         3344  
39 98     98   2865 use Carp;
  98         206  
  98         9299  
40              
41              
42 98         234 use constant ASCII => ref eval {
43 98         57318 require Encode;
44 98         1016073 Encode::find_encoding('ascii');
45 98     98   719 };
  98         242  
46              
47 98         196 use constant UTF8 => scalar eval { ## not UTF-EBCDIC [see Unicode TR#16 3.6]
48 98         377 Encode::encode_utf8( chr(182) ) eq pack( 'H*', 'C2B6' );
49 98     98   11556 };
  98         255  
50              
51 98     98   7018 use constant LIBIDN2 => defined eval { require Net::LibIDN2 };
  98         203  
  98         190  
  98         22444  
52 98     98   618 use constant IDN2FLAG => LIBIDN2 ? &Net::LibIDN2::IDN2_NFC_INPUT + &Net::LibIDN2::IDN2_NONTRANSITIONAL : 0;
  98         198  
  98         5753  
53 98     98   711 use constant LIBIDN => LIBIDN2 ? undef : defined eval { require Net::LibIDN };
  98         205  
  98         190  
  98         187865  
54              
55             # perlcc: address of encoding objects must be determined at runtime
56             my $ascii = ASCII ? Encode::find_encoding('ascii') : undef; # Osborn's Law:
57             my $utf8 = UTF8 ? Encode::find_encoding('utf8') : undef; # Variables won't; constants aren't.
58              
59              
60             =head1 METHODS
61              
62             =head2 new
63              
64             $object = Net::DNS::Domain->new('example.com');
65              
66             Creates a domain object which represents the DNS domain specified
67             by the character string argument. The argument consists of a
68             sequence of labels delimited by dots.
69              
70             A character preceded by \ represents itself, without any special
71             interpretation.
72              
73             Arbitrary 8-bit codes can be represented by \ followed by exactly
74             three decimal digits.
75             Character code points are ASCII, irrespective of the character
76             coding scheme employed by the underlying platform.
77              
78             Argument string literals should be delimited by single quotes to
79             avoid escape sequences being interpreted as octal character codes
80             by the Perl compiler.
81              
82             The character string presentation format follows the conventions
83             for zone files described in RFC1035.
84              
85             Users should be aware that non-ASCII domain names will be transcoded
86             to NFC before encoding, which is an irreversible process.
87              
88             =cut
89              
90             my ( %escape, %unescape ); ## precalculated ASCII escape tables
91              
92             our $ORIGIN;
93             my ( $cache1, $cache2, $limit ) = ( {}, {}, 100 );
94              
95             sub new {
96 4711     4711 1 39553 my ( $class, $s ) = @_;
97 4711 100       9588 croak 'domain identifier undefined' unless defined $s;
98              
99 4707   100     16197 my $index = join '', $s, $class, $ORIGIN || ''; # cache key
100 4707   100     14905 my $cache = $$cache1{$index} ||= $$cache2{$index}; # two layer cache
101 4707 100       12429 return $cache if defined $cache;
102              
103 2077 100       3888 ( $cache1, $cache2, $limit ) = ( {}, $cache1, 500 ) unless $limit--; # recycle cache
104              
105 2077         4142 my $self = bless {}, $class;
106              
107 2077         4402 $s =~ s/\\\\/\\092/g; # disguise escaped escape
108 2077         3111 $s =~ s/\\\./\\046/g; # disguise escaped dot
109              
110 2077 100       5081 my $label = $self->{label} = ( $s eq '@' ) ? [] : [split /\056/, _encode_utf8($s)];
111              
112 2077         5088 foreach (@$label) {
113 3979 100       7065 croak qq(empty label in "$s") unless length;
114              
115 3977         4639 if ( LIBIDN2 && UTF8 && /[^\000-\177]/ ) {
116             my $rc = 0;
117             $_ = Net::LibIDN2::idn2_to_ascii_8( $_, IDN2FLAG, $rc );
118             croak Net::LibIDN2::idn2_strerror($rc) unless $_;
119             }
120              
121 3977         4612 if ( LIBIDN && UTF8 && /[^\000-\177]/ ) {
122             $_ = Net::LibIDN::idn_to_ascii( $_, 'utf-8' );
123             croak 'name contains disallowed character' unless $_;
124             }
125              
126 3977         5630 s/\134([\060-\071]{3})/$unescape{$1}/eg; # restore numeric escapes
  174         568  
127 3977         5405 s/\134([^\134])/$1/g; # restore character escapes
128 3977         5195 s/\134(\134)/$1/g; # restore escaped escapes
129 3977 100       7770 croak qq(label too long in "$s") if length > 63;
130             }
131              
132 2074         3353 $$cache1{$index} = $self; # cache object reference
133              
134 2074 100       6816 return $self if $s =~ /\.$/; # fully qualified name
135 1702   100     5953 $self->{origin} = $ORIGIN || return $self; # dynamically scoped $ORIGIN
136 21         67 return $self;
137             }
138              
139              
140             =head2 name
141              
142             $name = $domain->name;
143              
144             Returns the domain name as a character string corresponding to the
145             "common interpretation" to which RFC1034, 3.1, paragraph 9 alludes.
146              
147             Character escape sequences are used to represent a dot inside a
148             domain name label and the escape character itself.
149              
150             Any non-printable code point is represented using the appropriate
151             numerical escape sequence.
152              
153             =cut
154              
155             sub name {
156 4877     4877 1 8023 my ($self) = @_;
157              
158 4877 100       14707 return $self->{name} if defined $self->{name};
159 1730 100       3828 return unless defined wantarray;
160              
161 1540         4897 my @label = shift->_wire;
162 1540 100       3583 return $self->{name} = '.' unless scalar @label;
163              
164 1505         2806 for (@label) {
165 4472         8508 s/([^\055\101-\132\141-\172\060-\071])/$escape{$1}/eg;
  217         608  
166             }
167              
168 1505         4476 return $self->{name} = _decode_ascii( join chr(46), @label );
169             }
170              
171              
172             =head2 fqdn
173              
174             $fqdn = $domain->fqdn;
175              
176             Returns a character string containing the fully qualified domain
177             name, including the trailing dot.
178              
179             =cut
180              
181             sub fqdn {
182 1383     1383 1 2295 my $name = &name;
183 1383 100       5932 return $name =~ /[.]$/ ? $name : "$name."; # append trailing dot
184             }
185              
186              
187             =head2 xname
188              
189             $xname = $domain->xname;
190              
191             Interprets an extended name containing Unicode domain name labels
192             encoded as Punycode A-labels.
193              
194             If decoding is not possible, the ACE encoded name is returned.
195              
196             =cut
197              
198             sub xname {
199 2     2 1 34 my $name = &name;
200              
201 2         4 if ( LIBIDN2 && UTF8 && $name =~ /xn--/i ) {
202             my $self = shift;
203             return $self->{xname} if defined $self->{xname};
204             my $u8 = Net::LibIDN2::idn2_to_unicode_88($name);
205             return $self->{xname} = $u8 ? $utf8->decode($u8) : $name;
206             }
207              
208 2         4 if ( LIBIDN && UTF8 && $name =~ /xn--/i ) {
209             my $self = shift;
210             return $self->{xname} if defined $self->{xname};
211             return $self->{xname} = $utf8->decode( Net::LibIDN::idn_to_unicode $name, 'utf-8' );
212             }
213 2         9 return $name;
214             }
215              
216              
217             =head2 label
218              
219             @label = $domain->label;
220              
221             Identifies the domain by means of a list of domain labels.
222              
223             =cut
224              
225             sub label {
226 154     154 1 336 my @label = shift->_wire;
227 154         272 for (@label) {
228 421         782 s/([^\055\101-\132\141-\172\060-\071])/$escape{$1}/eg;
  13         57  
229 421         618 _decode_ascii($_);
230             }
231 154         438 return @label;
232             }
233              
234              
235             =head2 string
236              
237             $string = $object->string;
238              
239             Returns a character string containing the fully qualified domain
240             name as it appears in a zone file.
241              
242             Characters which are recognised by RFC1035 zone file syntax are
243             represented by the appropriate escape sequence.
244              
245             =cut
246              
247 1371     1371 1 2430 sub string { return &fqdn }
248              
249              
250             =head2 origin
251              
252             $create = Net::DNS::Domain->origin( $ORIGIN );
253             $result = &$create( sub{ Net::DNS::RR->new( 'mx MX 10 a' ); } );
254             $expect = Net::DNS::RR->new( "mx.$ORIGIN. MX 10 a.$ORIGIN." );
255              
256             Class method which returns a reference to a subroutine wrapper
257             which executes a given constructor in a dynamically scoped context
258             where relative names become descendents of the specified $ORIGIN.
259              
260             =cut
261              
262             my $placebo = sub { my $constructor = shift; &$constructor; };
263              
264             sub origin {
265 146     146 1 1324 my ( $class, $name ) = @_;
266 146 100       496 my $domain = defined $name ? __PACKAGE__->new($name) : return $placebo;
267              
268             return sub { # closure w.r.t. $domain
269 38     38   68 my $constructor = shift;
270 38         54 local $ORIGIN = $domain; # dynamically scoped $ORIGIN
271 38         79 &$constructor;
272             }
273 13         68 }
274              
275              
276             ########################################
277              
278             sub _decode_ascii { ## ASCII to perl internal encoding
279 1926     1926   3368 local $_ = shift;
280              
281             # partial transliteration for non-ASCII character encodings
282             tr
283 1926         2499 [\040-\176\000-\377]
284             [ !"#$%&'()*+,\-./0-9:;<=>?@A-Z\[\\\]^_`a-z{|}~?] unless ASCII;
285              
286 1926         3173 my $z = length($_) - length($_); # pre-5.18 taint workaround
287 1926         14918 return ASCII ? substr( $ascii->decode($_), $z ) : $_;
288             }
289              
290              
291             sub _encode_utf8 { ## perl internal encoding to UTF8
292 2066     2066   3433 local $_ = shift;
293              
294             # partial transliteration for non-ASCII character encodings
295             tr
296 2066         2548 [ !"#$%&'()*+,\-./0-9:;<=>?@A-Z\[\\\]^_`a-z{|}~\000-\377]
297             [\040-\176\077] unless ASCII;
298              
299 2066         3229 my $z = length($_) - length($_); # pre-5.18 taint workaround
300 2066         13791 return ASCII ? substr( ( UTF8 ? $utf8 : $ascii )->encode($_), $z ) : $_;
301             }
302              
303              
304             sub _wire {
305 5112     5112   7149 my $self = shift;
306              
307 5112         7574 my $label = $self->{label};
308 5112         6834 my $origin = $self->{origin};
309 5112 100       15388 return ( @$label, $origin ? $origin->_wire : () );
310             }
311              
312              
313             %escape = eval { ## precalculated ASCII escape table
314             my %table = map { ( chr($_) => chr($_) ) } ( 0 .. 127 );
315              
316             foreach my $n ( 0 .. 32, 34, 92, 127 .. 255 ) { # \ddd
317             my $codepoint = sprintf( '%03u', $n );
318              
319             # transliteration for non-ASCII character encodings
320             $codepoint =~ tr [0-9] [\060-\071];
321              
322             $table{pack( 'C', $n )} = pack 'C a3', 92, $codepoint;
323             }
324              
325             foreach my $n ( 40, 41, 46, 59 ) { # character escape
326             $table{chr($n)} = pack( 'C2', 92, $n );
327             }
328              
329             return %table;
330             };
331              
332              
333             %unescape = eval { ## precalculated numeric escape table
334             my %table;
335              
336             foreach my $n ( 0 .. 255 ) {
337             my $key = sprintf( '%03u', $n );
338              
339             # transliteration for non-ASCII character encodings
340             $key =~ tr [0-9] [\060-\071];
341              
342             $table{$key} = pack 'C', $n;
343             }
344             $table{"\060\071\062"} = pack 'C2', 92, 92; # escaped escape
345              
346             return %table;
347             };
348              
349              
350             1;
351             __END__