File Coverage

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


line stmt bran path cond sub pod time code
1               package Net::DNS::Domain;
2                
3 100       100   212064 use strict;
  100           146  
  100           2957  
4 100       100   377 use warnings;
  100           140  
  100           6122  
5                
6               our $VERSION = (qw$Id: Domain.pm 2002 2025-01-07 09:57:46Z 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 100       100   1228 use integer;
  100           154  
  100           445  
39 100       100   1653 use Carp;
  100           180  
  100           9666  
40                
41                
42 100           155 use constant ASCII => ref eval {
43 100           45423 require Encode;
44 100           1388636 Encode::find_encoding('ascii');
45 100       100   542 };
  100           145  
46                
47 100           161 use constant UTF8 => scalar eval { ## not UTF-EBCDIC [see Unicode TR#16 3.6]
48 100           6965 Encode::encode_utf8( chr(182) ) eq pack( 'H*', 'C2B6' );
49 100       100   12798 };
  100           232  
50                
51 100       100   507 use constant LIBIDN2 => defined eval { require Net::LibIDN2 };
  100           158  
  100           166  
  100           17351  
52 100       100   454 use constant IDN2FLAG => LIBIDN2 ? &Net::LibIDN2::IDN2_NFC_INPUT + &Net::LibIDN2::IDN2_NONTRANSITIONAL : 0;
  100           181  
  100           5547  
53 100       100   465 use constant LIBIDN => LIBIDN2 ? undef : defined eval { require Net::LibIDN };
  100           142  
  100           165  
  100           174327  
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 4730       4730 1 37856 my ( $class, $s ) = @_;
97 4730 100         7683 croak 'domain identifier undefined' unless defined $s;
98                
99 4726     100     13261 my $index = join '', $s, $class, $ORIGIN || ''; # cache key
100 4726     100     12007 my $cache = $$cache1{$index} ||= $$cache2{$index}; # two layer cache
101 4726 100         10322 return $cache if defined $cache;
102                
103 2087 100         3052 ( $cache1, $cache2, $limit ) = ( {}, $cache1, 500 ) unless $limit--; # recycle cache
104                
105 2087           3122 my $self = bless {}, $class;
106                
107 2087           3027 $s =~ s/\\\\/\\092/g; # disguise escaped escape
108 2087           2662 $s =~ s/\\\./\\046/g; # disguise escaped dot
109                
110 2087 100         4072 my $label = $self->{label} = ( $s eq '@' ) ? [] : [split /\056/, _encode_utf8($s)];
111                
112 2087           3476 foreach (@$label) {
113 3950 100         5565 croak qq(empty label in "$s") unless length;
114                
115 3948           3629 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 3948           3478 if ( LIBIDN && UTF8 && /[^\000-\177]/ ) {
122               $_ = Net::LibIDN::idn_to_ascii( $_, 'utf-8' );
123               croak 'name contains disallowed character' unless $_;
124               }
125                
126 3948           4344 s/\134([\060-\071]{3})/$unescape{$1}/eg; # restore numeric escapes
  174           682  
127 3948           5005 s/\134([^\134])/$1/g; # restore character escapes
128 3948           4138 s/\134(\134)/$1/g; # restore escaped escapes
129 3948 100         6028 croak qq(label too long in "$s") if length > 63;
130               }
131                
132 2084           2578 $$cache1{$index} = $self; # cache object reference
133                
134 2084 100         4909 return $self if $s =~ /\.$/; # fully qualified name
135 1705     100     4528 $self->{origin} = $ORIGIN || return $self; # dynamically scoped $ORIGIN
136 21           63 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 2790       2790 1 4195 my ($self) = @_;
157                
158 2790 100         7671 return $self->{name} if defined $self->{name};
159 1624 100         2744 return unless defined wantarray;
160                
161 1431           2623 my @label = shift->_wire;
162 1431 100         2507 return $self->{name} = '.' unless scalar @label;
163                
164 1405           1930 for (@label) {
165 4113           6057 s/([^\055\101-\132\141-\172\060-\071])/$escape{$1}/eg;
  219           800  
166               }
167                
168 1405           3072 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 1355       1355 1 1996 my $name = &name;
183 1355 100         4786 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 8 my $name = &name;
200                
201 2           3 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           3 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 300 my @label = shift->_wire;
227 154           207 for (@label) {
228 421           588 s/([^\055\101-\132\141-\172\060-\071])/$escape{$1}/eg;
  13           64  
229 421           513 _decode_ascii($_);
230               }
231 154           338 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 1339       1339 1 2283 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 1684 my ( $class, $name ) = @_;
266 146 100         426 my $domain = defined $name ? __PACKAGE__->new($name) : return $placebo;
267                
268               return sub { # closure w.r.t. $domain
269 38       38   56 my $constructor = shift;
270 38           45 local $ORIGIN = $domain; # dynamically scoped $ORIGIN
271 38           52 &$constructor;
272               }
273 13           62 }
274                
275                
276               ########################################
277                
278               sub _decode_ascii { ## ASCII to perl internal encoding
279 1826       1826   2460 local $_ = shift;
280                
281               # partial transliteration for non-ASCII character encodings
282               tr
283 1826           1946 [\040-\176\000-\377]
284               [ !"#$%&'()*+,\-./0-9:;<=>?@A-Z\[\\\]^_`a-z{|}~?] unless ASCII;
285                
286 1826           2212 my $z = length($_) - length($_); # pre-5.18 taint workaround
287 1826           10118 return ASCII ? substr( $ascii->decode($_), $z ) : $_;
288               }
289                
290                
291               sub _encode_utf8 { ## perl internal encoding to UTF8
292 2076       2076   2605 local $_ = shift;
293                
294               # partial transliteration for non-ASCII character encodings
295               tr
296 2076           2006 [ !"#$%&'()*+,\-./0-9:;<=>?@A-Z\[\\\]^_`a-z{|}~\000-\377]
297               [\040-\176\077] unless ASCII;
298                
299 2076           2638 my $z = length($_) - length($_); # pre-5.18 taint workaround
300 2076           11252 return ASCII ? substr( ( UTF8 ? $utf8 : $ascii )->encode($_), $z ) : $_;
301               }
302                
303                
304               sub _wire {
305 4889       4889   5383 my $self = shift;
306                
307 4889           5891 my $label = $self->{label};
308 4889           5619 my $origin = $self->{origin};
309 4889 100         11644 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__