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   1796 use strict;
  98         219  
  98         2943  
4 98     98   518 use warnings;
  98         251  
  98         4809  
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   1692 use integer;
  98         262  
  98         3494  
39 98     98   2936 use Carp;
  98         235  
  98         9805  
40              
41              
42 98         255 use constant ASCII => ref eval {
43 98         60195 require Encode;
44 98         1057504 Encode::find_encoding('ascii');
45 98     98   785 };
  98         260  
46              
47 98         207 use constant UTF8 => scalar eval { ## not UTF-EBCDIC [see Unicode TR#16 3.6]
48 98         435 Encode::encode_utf8( chr(182) ) eq pack( 'H*', 'C2B6' );
49 98     98   11677 };
  98         239  
50              
51 98     98   7226 use constant LIBIDN2 => defined eval { require Net::LibIDN2 };
  98         214  
  98         216  
  98         23222  
52 98     98   647 use constant IDN2FLAG => LIBIDN2 ? &Net::LibIDN2::IDN2_NFC_INPUT + &Net::LibIDN2::IDN2_NONTRANSITIONAL : 0;
  98         204  
  98         6085  
53 98     98   753 use constant LIBIDN => LIBIDN2 ? undef : defined eval { require Net::LibIDN };
  98         223  
  98         183  
  98         195002  
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 4704     4704 1 39966 my ( $class, $s ) = @_;
97 4704 100       9816 croak 'domain identifier undefined' unless defined $s;
98              
99 4700   100     16636 my $index = join '', $s, $class, $ORIGIN || ''; # cache key
100 4700   100     15691 my $cache = $$cache1{$index} ||= $$cache2{$index}; # two layer cache
101 4700 100       12630 return $cache if defined $cache;
102              
103 2076 100       3749 ( $cache1, $cache2, $limit ) = ( {}, $cache1, 500 ) unless $limit--; # recycle cache
104              
105 2076         4215 my $self = bless {}, $class;
106              
107 2076         4397 $s =~ s/\\\\/\\092/g; # disguise escaped escape
108 2076         3048 $s =~ s/\\\./\\046/g; # disguise escaped dot
109              
110 2076 100       5259 my $label = $self->{label} = ( $s eq '@' ) ? [] : [split /\056/, _encode_utf8($s)];
111              
112 2076         5045 foreach (@$label) {
113 3974 100       7118 croak qq(empty label in "$s") unless length;
114              
115 3972         4687 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 3972         4475 if ( LIBIDN && UTF8 && /[^\000-\177]/ ) {
122             $_ = Net::LibIDN::idn_to_ascii( $_, 'utf-8' );
123             croak 'name contains disallowed character' unless $_;
124             }
125              
126 3972         5625 s/\134([\060-\071]{3})/$unescape{$1}/eg; # restore numeric escapes
  174         553  
127 3972         5668 s/\134([^\134])/$1/g; # restore character escapes
128 3972         5103 s/\134(\134)/$1/g; # restore escaped escapes
129 3972 100       7699 croak qq(label too long in "$s") if length > 63;
130             }
131              
132 2073         3263 $$cache1{$index} = $self; # cache object reference
133              
134 2073 100       6498 return $self if $s =~ /\.$/; # fully qualified name
135 1701   100     6188 $self->{origin} = $ORIGIN || return $self; # dynamically scoped $ORIGIN
136 21         69 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 4827     4827 1 8562 my ($self) = @_;
157              
158 4827 100       15363 return $self->{name} if defined $self->{name};
159 1691 100       4624 return unless defined wantarray;
160              
161 1501         3319 my @label = shift->_wire;
162 1501 100       3630 return $self->{name} = '.' unless scalar @label;
163              
164 1466         2859 for (@label) {
165 4329         8667 s/([^\055\101-\132\141-\172\060-\071])/$escape{$1}/eg;
  217         642  
166             }
167              
168 1466         4593 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 2199 my $name = &name;
183 1383 100       5817 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         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         2 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         11 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 371 my @label = shift->_wire;
227 154         282 for (@label) {
228 421         800 s/([^\055\101-\132\141-\172\060-\071])/$escape{$1}/eg;
  13         51  
229 421         627 _decode_ascii($_);
230             }
231 154         456 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 2638 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 1414 my ( $class, $name ) = @_;
266 146 100       500 my $domain = defined $name ? __PACKAGE__->new($name) : return $placebo;
267              
268             return sub { # closure w.r.t. $domain
269 38     38   67 my $constructor = shift;
270 38         65 local $ORIGIN = $domain; # dynamically scoped $ORIGIN
271 38         74 &$constructor;
272             }
273 13         81 }
274              
275              
276             ########################################
277              
278             sub _decode_ascii { ## ASCII to perl internal encoding
279 1887     1887   3314 local $_ = shift;
280              
281             # partial transliteration for non-ASCII character encodings
282             tr
283 1887         2580 [\040-\176\000-\377]
284             [ !"#$%&'()*+,\-./0-9:;<=>?@A-Z\[\\\]^_`a-z{|}~?] unless ASCII;
285              
286 1887         3049 my $z = length($_) - length($_); # pre-5.18 taint workaround
287 1887         13808 return ASCII ? substr( $ascii->decode($_), $z ) : $_;
288             }
289              
290              
291             sub _encode_utf8 { ## perl internal encoding to UTF8
292 2065     2065   3460 local $_ = shift;
293              
294             # partial transliteration for non-ASCII character encodings
295             tr
296 2065         2485 [ !"#$%&'()*+,\-./0-9:;<=>?@A-Z\[\\\]^_`a-z{|}~\000-\377]
297             [\040-\176\077] unless ASCII;
298              
299 2065         3266 my $z = length($_) - length($_); # pre-5.18 taint workaround
300 2065         14456 return ASCII ? substr( ( UTF8 ? $utf8 : $ascii )->encode($_), $z ) : $_;
301             }
302              
303              
304             sub _wire {
305 4851     4851   7087 my $self = shift;
306              
307 4851         7360 my $label = $self->{label};
308 4851         6692 my $origin = $self->{origin};
309 4851 100       15423 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__