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   1686 use strict;
  98         203  
  98         2976  
4 98     98   478 use warnings;
  98         194  
  98         4613  
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   1525 use integer;
  98         213  
  98         3357  
39 98     98   3002 use Carp;
  98         215  
  98         9419  
40              
41              
42 98         226 use constant ASCII => ref eval {
43 98         56618 require Encode;
44 98         1025533 Encode::find_encoding('ascii');
45 98     98   738 };
  98         243  
46              
47 98         212 use constant UTF8 => scalar eval { ## not UTF-EBCDIC [see Unicode TR#16 3.6]
48 98         417 Encode::encode_utf8( chr(182) ) eq pack( 'H*', 'C2B6' );
49 98     98   11365 };
  98         218  
50              
51 98     98   6973 use constant LIBIDN2 => defined eval { require Net::LibIDN2 };
  98         216  
  98         185  
  98         22929  
52 98     98   591 use constant IDN2FLAG => LIBIDN2 ? &Net::LibIDN2::IDN2_NFC_INPUT + &Net::LibIDN2::IDN2_NONTRANSITIONAL : 0;
  98         213  
  98         5958  
53 98     98   719 use constant LIBIDN => LIBIDN2 ? undef : defined eval { require Net::LibIDN };
  98         193  
  98         204  
  98         189932  
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 40371 my ( $class, $s ) = @_;
97 4711 100       9941 croak 'domain identifier undefined' unless defined $s;
98              
99 4707   100     16798 my $index = join '', $s, $class, $ORIGIN || ''; # cache key
100 4707   100     15711 my $cache = $$cache1{$index} ||= $$cache2{$index}; # two layer cache
101 4707 100       12579 return $cache if defined $cache;
102              
103 2077 100       3796 ( $cache1, $cache2, $limit ) = ( {}, $cache1, 500 ) unless $limit--; # recycle cache
104              
105 2077         4172 my $self = bless {}, $class;
106              
107 2077         4288 $s =~ s/\\\\/\\092/g; # disguise escaped escape
108 2077         3025 $s =~ s/\\\./\\046/g; # disguise escaped dot
109              
110 2077 100       5165 my $label = $self->{label} = ( $s eq '@' ) ? [] : [split /\056/, _encode_utf8($s)];
111              
112 2077         5236 foreach (@$label) {
113 3978 100       7325 croak qq(empty label in "$s") unless length;
114              
115 3976         4568 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 3976         4445 if ( LIBIDN && UTF8 && /[^\000-\177]/ ) {
122             $_ = Net::LibIDN::idn_to_ascii( $_, 'utf-8' );
123             croak 'name contains disallowed character' unless $_;
124             }
125              
126 3976         5520 s/\134([\060-\071]{3})/$unescape{$1}/eg; # restore numeric escapes
  174         629  
127 3976         5290 s/\134([^\134])/$1/g; # restore character escapes
128 3976         5039 s/\134(\134)/$1/g; # restore escaped escapes
129 3976 100       7592 croak qq(label too long in "$s") if length > 63;
130             }
131              
132 2074         3319 $$cache1{$index} = $self; # cache object reference
133              
134 2074 100       6380 return $self if $s =~ /\.$/; # fully qualified name
135 1702   100     6363 $self->{origin} = $ORIGIN || return $self; # dynamically scoped $ORIGIN
136 21         71 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 4928     4928 1 8458 my ($self) = @_;
157              
158 4928 100       15057 return $self->{name} if defined $self->{name};
159 1733 100       3625 return unless defined wantarray;
160              
161 1543         3339 my @label = shift->_wire;
162 1543 100       3680 return $self->{name} = '.' unless scalar @label;
163              
164 1508         2869 for (@label) {
165 4479         8303 s/([^\055\101-\132\141-\172\060-\071])/$escape{$1}/eg;
  217         711  
166             }
167              
168 1508         4577 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 2290 my $name = &name;
183 1383 100       6131 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         6 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 325 my @label = shift->_wire;
227 154         258 for (@label) {
228 421         769 s/([^\055\101-\132\141-\172\060-\071])/$escape{$1}/eg;
  13         52  
229 421         600 _decode_ascii($_);
230             }
231 154         436 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 2527 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 1353 my ( $class, $name ) = @_;
266 146 100       478 my $domain = defined $name ? __PACKAGE__->new($name) : return $placebo;
267              
268             return sub { # closure w.r.t. $domain
269 38     38   84 my $constructor = shift;
270 38         57 local $ORIGIN = $domain; # dynamically scoped $ORIGIN
271 38         78 &$constructor;
272             }
273 13         68 }
274              
275              
276             ########################################
277              
278             sub _decode_ascii { ## ASCII to perl internal encoding
279 1929     1929   3202 local $_ = shift;
280              
281             # partial transliteration for non-ASCII character encodings
282             tr
283 1929         2456 [\040-\176\000-\377]
284             [ !"#$%&'()*+,\-./0-9:;<=>?@A-Z\[\\\]^_`a-z{|}~?] unless ASCII;
285              
286 1929         3097 my $z = length($_) - length($_); # pre-5.18 taint workaround
287 1929         14853 return ASCII ? substr( $ascii->decode($_), $z ) : $_;
288             }
289              
290              
291             sub _encode_utf8 { ## perl internal encoding to UTF8
292 2066     2066   3380 local $_ = shift;
293              
294             # partial transliteration for non-ASCII character encodings
295             tr
296 2066         2387 [ !"#$%&'()*+,\-./0-9:;<=>?@A-Z\[\\\]^_`a-z{|}~\000-\377]
297             [\040-\176\077] unless ASCII;
298              
299 2066         3208 my $z = length($_) - length($_); # pre-5.18 taint workaround
300 2066         13911 return ASCII ? substr( ( UTF8 ? $utf8 : $ascii )->encode($_), $z ) : $_;
301             }
302              
303              
304             sub _wire {
305 5076     5076   6881 my $self = shift;
306              
307 5076         7545 my $label = $self->{label};
308 5076         7220 my $origin = $self->{origin};
309 5076 100       15777 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__