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 100     100   466941 use strict;
  100         202  
  100         4982  
4 100     100   537 use warnings;
  100         201  
  100         8210  
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   1675 use integer;
  100         200  
  100         717  
39 100     100   2556 use Carp;
  100         204  
  100         11785  
40              
41              
42 100         245 use constant ASCII => ref eval {
43 100         81570 require Encode;
44 100         1906789 Encode::find_encoding('ascii');
45 100     100   719 };
  100         192  
46              
47 100         248 use constant UTF8 => scalar eval { ## not UTF-EBCDIC [see Unicode TR#16 3.6]
48 100         8344 Encode::encode_utf8( chr(182) ) eq pack( 'H*', 'C2B6' );
49 100     100   21482 };
  100         330  
50              
51 100     100   626 use constant LIBIDN2 => defined eval { require Net::LibIDN2 };
  100         201  
  100         203  
  100         22762  
52 100     100   543 use constant IDN2FLAG => LIBIDN2 ? &Net::LibIDN2::IDN2_NFC_INPUT + &Net::LibIDN2::IDN2_NONTRANSITIONAL : 0;
  100         227  
  100         7629  
53 100     100   636 use constant LIBIDN => LIBIDN2 ? undef : defined eval { require Net::LibIDN };
  100         205  
  100         232  
  100         245550  
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 4737     4737 1 57789 my ( $class, $s ) = @_;
97 4737 100       11492 croak 'domain identifier undefined' unless defined $s;
98              
99 4733   100     23778 my $index = join '', $s, $class, $ORIGIN || ''; # cache key
100 4733   100     22234 my $cache = $$cache1{$index} ||= $$cache2{$index}; # two layer cache
101 4733 100       15877 return $cache if defined $cache;
102              
103 2087 100       5380 ( $cache1, $cache2, $limit ) = ( {}, $cache1, 500 ) unless $limit--; # recycle cache
104              
105 2087         5422 my $self = bless {}, $class;
106              
107 2087         4892 $s =~ s/\\\\/\\092/g; # disguise escaped escape
108 2087         3860 $s =~ s/\\\./\\046/g; # disguise escaped dot
109              
110 2087 100       6424 my $label = $self->{label} = ( $s eq '@' ) ? [] : [split /\056/, _encode_utf8($s)];
111              
112 2087         5612 foreach (@$label) {
113 3952 100       8778 croak qq(empty label in "$s") unless length;
114              
115 3950         5548 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 3950         5533 if ( LIBIDN && UTF8 && /[^\000-\177]/ ) {
122             $_ = Net::LibIDN::idn_to_ascii( $_, 'utf-8' );
123             croak 'name contains disallowed character' unless $_;
124             }
125              
126 3950         7404 s/\134([\060-\071]{3})/$unescape{$1}/eg; # restore numeric escapes
  174         683  
127 3950         6447 s/\134([^\134])/$1/g; # restore character escapes
128 3950         6318 s/\134(\134)/$1/g; # restore escaped escapes
129 3950 100       9388 croak qq(label too long in "$s") if length > 63;
130             }
131              
132 2084         4324 $$cache1{$index} = $self; # cache object reference
133              
134 2084 100       7232 return $self if $s =~ /\.$/; # fully qualified name
135 1705   100     8709 $self->{origin} = $ORIGIN || return $self; # dynamically scoped $ORIGIN
136 21         76 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 2823     2823 1 8190 my ($self) = @_;
157              
158 2823 100       9727 return $self->{name} if defined $self->{name};
159 1657 100       4350 return unless defined wantarray;
160              
161 1464         5271 my @label = shift->_wire;
162 1464 100       3755 return $self->{name} = '.' unless scalar @label;
163              
164 1438         3108 for (@label) {
165 4225         16195 s/([^\055\101-\132\141-\172\060-\071])/$escape{$1}/eg;
  219         813  
166             }
167              
168 1438         5160 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 2605 my $name = &name;
183 1355 100       6929 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         5 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         12 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 460 my @label = shift->_wire;
227 154         314 for (@label) {
228 421         943 s/([^\055\101-\132\141-\172\060-\071])/$escape{$1}/eg;
  13         65  
229 421         775 _decode_ascii($_);
230             }
231 154         650 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 3222 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 2198 my ( $class, $name ) = @_;
266 146 100       534 my $domain = defined $name ? __PACKAGE__->new($name) : return $placebo;
267              
268             return sub { # closure w.r.t. $domain
269 38     38   70 my $constructor = shift;
270 38         67 local $ORIGIN = $domain; # dynamically scoped $ORIGIN
271 38         70 &$constructor;
272             }
273 13         88 }
274              
275              
276             ########################################
277              
278             sub _decode_ascii { ## ASCII to perl internal encoding
279 1859     1859   3697 local $_ = shift;
280              
281             # partial transliteration for non-ASCII character encodings
282             tr
283 1859         2826 [\040-\176\000-\377]
284             [ !"#$%&'()*+,\-./0-9:;<=>?@A-Z\[\\\]^_`a-z{|}~?] unless ASCII;
285              
286 1859         3507 my $z = length($_) - length($_); # pre-5.18 taint workaround
287 1859         17331 return ASCII ? substr( $ascii->decode($_), $z ) : $_;
288             }
289              
290              
291             sub _encode_utf8 { ## perl internal encoding to UTF8
292 2076     2076   4051 local $_ = shift;
293              
294             # partial transliteration for non-ASCII character encodings
295             tr
296 2076         2959 [ !"#$%&'()*+,\-./0-9:;<=>?@A-Z\[\\\]^_`a-z{|}~\000-\377]
297             [\040-\176\077] unless ASCII;
298              
299 2076         3956 my $z = length($_) - length($_); # pre-5.18 taint workaround
300 2076         18330 return ASCII ? substr( ( UTF8 ? $utf8 : $ascii )->encode($_), $z ) : $_;
301             }
302              
303              
304             sub _wire {
305 4994     4994   7892 my $self = shift;
306              
307 4994         8413 my $label = $self->{label};
308 4994         7962 my $origin = $self->{origin};
309 4994 100       17323 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__