File Coverage

blib/lib/Net/DNS/RR/OPT.pm
Criterion Covered Total %
statement 306 306 100.0
branch 114 114 100.0
condition 32 32 100.0
subroutine 56 56 100.0
pod 11 14 100.0
total 519 522 100.0


line stmt bran cond sub pod time code
1             package Net::DNS::RR::OPT;
2              
3 12     12   92 use strict;
  12         25  
  12         527  
4 12     12   97 use warnings;
  12         19  
  12         1262  
5             our $VERSION = (qw$Id: OPT.pm 2005 2025-01-28 13:22:10Z willem $)[2];
6              
7 12     12   83 use base qw(Net::DNS::RR);
  12         28  
  12         1263  
8              
9              
10             =head1 NAME
11              
12             Net::DNS::RR::OPT - DNS OPT resource record
13              
14             =cut
15              
16 12     12   90 use integer;
  12         19  
  12         90  
17              
18 12     12   407 use Carp;
  12         25  
  12         1026  
19 12     12   64 use Net::DNS::Parameters qw(:rcode :ednsoption);
  12         23  
  12         2406  
20              
21 12     12   99 use constant UTIL => scalar eval { require Scalar::Util; Scalar::Util->can('isdual') };
  12         21  
  12         23  
  12         39  
  12         1156  
22              
23 12     12   65 use constant OPT => Net::DNS::Parameters::typebyname qw(OPT);
  12         20  
  12         44  
24              
25             require Net::DNS::DomainName;
26             require Net::DNS::RR::A;
27             require Net::DNS::RR::AAAA;
28             require Net::DNS::Text;
29              
30              
31             sub _decode_rdata { ## decode rdata from wire-format octet string
32 55     55   153 my ( $self, $data, $offset ) = @_;
33              
34 55         275 my $class = delete $self->{class}; # OPT redefines CLASS and TTL fields
35 55 100       320 $self->udpsize($class) if defined $class;
36              
37 55         271 my $ttl = delete $self->{ttl};
38 55 100       331 $self->_ttl($ttl) if defined $ttl;
39              
40 55         153 my $limit = $offset + $self->{rdlength} - 4;
41 55         220 while ( $offset <= $limit ) {
42 31         83 my ( $code, $length ) = unpack "\@$offset nn", $$data;
43 31         75 my $value = unpack "\@$offset x4 a$length", $$data;
44 31 100       87 my @value = map { ref($_) ? @$_ : defined($_) ? $_ : () } $self->{option}{$code}, $value;
  62 100       162  
45 31 100       80 $self->{option}{$code} = ( scalar(@value) == 1 ) ? $value : \@value;
46 31         78 $offset += $length + 4;
47             }
48 55         146 return;
49             }
50              
51              
52             sub _encode_rdata { ## encode rdata as wire-format octet string
53 62     62   228 my $self = shift;
54              
55 62   100     343 my $option = $self->{option} || {};
56 62         1058 my @option = $self->options;
57 62         242 foreach my $item (@option) {
58 26 100       72 my @value = map { ref($_) ? @$_ : $_ } $option->{$item};
  26         71  
59 26         46 $item = join '', map { pack( 'nna*', $item, length($_), $_ ) } @value;
  28         111  
60             }
61 62         237 return join '', @option;
62             }
63              
64              
65             sub encode { ## override RR method
66 61     61 1 192 my $self = shift;
67 61         235 my $data = $self->_encode_rdata;
68 61         238 return pack 'C n n N na*', 0, OPT, $self->udpsize, $self->_ttl, length($data), $data;
69             }
70              
71              
72             sub string { ## override RR method
73 6     6 1 2068 my @line = split /[\r\n]+/, shift->json;
74 6         19 return join '', map {";;$_\n"} @line;
  60         225  
75             }
76              
77             sub class { ## override RR method
78 2     2 1 871 my ( $self, @value ) = @_;
79 2         15 $self->_deprecate(qq[please use "UDPsize()"]);
80 2         8 return $self->udpsize(@value);
81             }
82              
83             sub ttl { ## override RR method
84 2     2 1 1681 my ( $self, @value ) = @_;
85 2         10 $self->_deprecate(qq[please use "flags()", "rcode()" or "version()"]);
86 2         6 return $self->_ttl(@value);
87             }
88              
89             sub _ttl {
90 117     117   441 my ( $self, @value ) = @_;
91 117         289 for (@value) {
92 56         4786 @{$self}{qw(rcode version flags)} = unpack 'C2n', pack( 'N', $_ );
  56         320  
93 56         178 $self->{rcode} = $self->{rcode} << 4;
94 56         126 return;
95             }
96 61         209 return unpack 'N', pack( 'C2n', $self->rcode >> 4, $self->version, $self->flags );
97             }
98              
99             sub generic { ## override RR method
100 1     1 1 2 my $self = shift;
101 1         4 local $self->{class} = $self->udpsize;
102 1         4 my @xttl = ( $self->rcode >> 4, $self->version, $self->flags );
103 1         6 local $self->{ttl} = unpack 'N', pack( 'C2n', @xttl );
104 1         7 return $self->SUPER::generic;
105             }
106              
107             sub token { ## override RR method
108 1     1 1 4 return grep { !m/^[()]$/ } split /\s+/, &generic;
  6         25  
109             }
110              
111             sub json {
112 6     6 0 12 my $self = shift; # uncoverable pod
113              
114 6         17 my $version = $self->version;
115 6 100       26 unless ( $version == 0 ) {
116 1         5 my $content = unpack 'H*', $self->encode;
117 1         11 return <<"QQ";
118             { "EDNS-VERSION": $version,
119             "BASE16": "$content"
120             }
121             QQ
122             }
123              
124 5         15 my $flags = $self->flags;
125 5         14 my $rcode = $self->rcode;
126 5         12 my $size = $self->udpsize;
127 5         15 my @format = map { join( "\n\t\t\t", $self->_format_option($_) ) } $self->options;
  24         67  
128 5 100       20 my @indent = scalar(@format) ? "\n\t\t" : ();
129 5         36 my @option = join ",\n\t\t", @format;
130              
131 5         1392 return <<"QQ";
132             { "EDNS-VERSION": $version,
133             "FLAGS": $flags,
134             "RCODE": $rcode,
135             "UDPSIZE": $size,
136             "OPTIONS": [@indent@option ]
137             }
138             QQ
139             }
140              
141              
142             sub version {
143 88     88 1 262 my ( $self, @value ) = @_;
144 88         237 for (@value) { $self->{version} = 0 + $_ }
  3         7  
145 88   100     555 return $self->{version} || 0;
146             }
147              
148              
149             sub udpsize {
150 222     222 0 604 my ( $self, @value ) = @_; # uncoverable pod
151 222 100       625 for (@value) { $self->{udpsize} = ( $_ > 512 ) ? $_ : 0 }
  151         765  
152 222   100     958 return $self->{udpsize} || 0;
153             }
154              
155             sub size {
156 2     2 0 1263 my ( $self, @value ) = @_; # uncoverable pod
157 2         11 $self->_deprecate(qq[size() is an alias of "UDPsize()"]);
158 2         7 return $self->udpsize(@value);
159             }
160              
161              
162             sub rcode {
163 510     510 1 1228 my ( $self, @value ) = @_;
164 510 100       1192 for (@value) { $self->{rcode} = ( $_ < 16 ) ? 0 : $_ } # discard non-EDNS rcodes 1 .. 15
  229         721  
165 510   100     2185 return $self->{rcode} || 0;
166             }
167              
168              
169             sub flags {
170 166     166 1 317 my ( $self, @value ) = @_;
171 166         320 for (@value) { $self->{flags} = 0 + $_ }
  16         40  
172 166   100     1187 return $self->{flags} || 0;
173             }
174              
175              
176             sub options {
177 72     72 1 150 my $self = shift;
178 72   100     310 my $option = $self->{option} || {};
179 72 100       376 @{$self->{index}} = sort { $a <=> $b } keys %$option unless defined $self->{index};
  39         178  
  159         274  
180 72         135 return @{$self->{index}};
  72         330  
181             }
182              
183             sub option {
184 154     154 1 17352 my ( $self, $name, @value ) = @_;
185 154         510 my $number = ednsoptionbyname($name);
186 154 100       472 return $self->_get_option($number) unless scalar @value;
187 79         231 my $value = $self->_set_option( $number, @value );
188 78 100       429 return $@ ? croak( ( split /\sat/i, $@ )[0] ) : $value;
189             }
190              
191              
192             ########################################
193              
194             sub _get_option {
195 99     99   217 my ( $self, $number ) = @_;
196              
197 99   100     262 my $options = $self->{option} || {};
198 99 100       233 my @payload = map { ref($_) ? @$_ : $_ } $options->{$number};
  99         367  
199 99 100       337 return shift @payload unless wantarray;
200 48         171 my $optname = ednsoptionbyval($number);
201 48         136 my $package = join '::', __PACKAGE__, $optname;
202 48         162 $package =~ s/-/_/g;
203 48         442 my $structured = $package->can('_decompose');
204 48         125 foreach my $value (@payload) {
205 52         79 my @value;
206 52 100       110 if ( length $value ) {
207 48 100       142 @value = eval { $package->_decompose($value) } if $structured;
  39         126  
208 48 100       188 @value = {BASE16 => unpack 'H*', $value} unless scalar @value;
209 48 100       130 warn $@ if $@;
210             } else {
211 4 100       18 @value = $structured ? {'OPTION-LENGTH' => 0} : '';
212             }
213 52         183 $value = {$optname => @value};
214             }
215 48         165 return @payload;
216             }
217              
218              
219             sub _set_option {
220 79     79   175 my ( $self, $number, @value ) = @_;
221 79         149 my ($arg) = @value;
222              
223 79   100     220 my $options = $self->{option} || {};
224 79         170 delete $options->{$number};
225 79         135 delete $self->{index};
226 79 100       219 delete $self->{option} unless scalar( keys %$options );
227              
228 79 100       176 return unless defined $arg;
229 78         149 $self->{option} = $options;
230              
231 78 100       188 if ( ref($arg) eq 'HASH' ) {
232 49         115 for ( keys %$arg ) { $$arg{uc $_} = $$arg{$_} } # tolerate mixed case
  63         192  
233 49         94 my $length = $$arg{'OPTION-LENGTH'};
234 49         87 my $octets = $$arg{'OPTION-DATA'};
235 49 100       147 $octets = pack 'H*', $$arg{'BASE16'} if defined $$arg{'BASE16'};
236 49 100 100     129 $octets = '' if defined($length) && $length == 0;
237 49 100       177 return $options->{$number} = $octets if defined $octets;
238             }
239              
240 43         105 my $option = ednsoptionbyval($number);
241 43         121 my $package = join '::', __PACKAGE__, $option;
242 43         145 $package =~ s/-/_/g;
243 43 100 100     626 return eval { $options->{$number} = $package->_compose(@value) } if length($arg) && $package->can('_compose');
  38         125  
244              
245 5 100       166 croak "unable to compose option $number" if ref($arg);
246 4         17 return $options->{$number} = $arg;
247             }
248              
249              
250             sub _specified {
251 217     217   896 my $self = shift;
252 217         4554 return scalar grep { $self->{$_} } qw(udpsize flags rcode option);
  868         5316  
253             }
254              
255              
256             sub _format_option {
257 24     24   83 my ( $self, $number ) = @_;
258 24         94 my @option = $self->_get_option($number);
259 24         45 return map { Net::DNS::RR::_wrap( _JSONify($_) ) } @option;
  26         54  
260             }
261              
262              
263             sub _JSONify {
264 115     115   1547 my $value = shift;
265 115 100       246 return 'null' unless defined $value;
266              
267 114 100       276 if ( ref($value) eq 'HASH' ) {
268 48         143 my @tags = sort keys %$value;
269 48         89 my $tail = pop @tags;
270 48 100       109 for ( $$value{BASE16} ) { $_ = pack( 'U0a*', $_ ) if defined } # mark as UTF-8
  48         131  
271 48         97 my @body = map { my @x = ( qq("$_":), _JSONify( $$value{$_} ) ); $x[-1] .= ','; @x } @tags;
  13         36  
  13         25  
  13         40  
272 48         154 push @body, ( qq("$tail":), _JSONify( $$value{$tail} ) );
273 48         108 $body[0] = '{' . $body[0];
274 48         77 $body[-1] .= '}';
275 48         229 return @body;
276             }
277              
278 66 100       150 if ( ref($value) eq 'ARRAY' ) {
279 4         13 my @array = @$value;
280 4         10 my @tail = map { _JSONify($_) } grep {defined} pop @array;
  4         12  
  4         12  
281 4         9 my @body = map { my @x = _JSONify($_); $x[-1] .= ','; @x } @array;
  10         19  
  10         16  
  10         44  
282 4         24 return ( '[', @body, @tail, ']' );
283             }
284              
285 62         138 my $string = "$value"; ## stringify, then use isdual() as discriminant
286 62 100       209 return $string if UTIL && Scalar::Util::isdual($value); # native numeric representation
287 32         63 for ($string) {
288 32 100       88 unless ( utf8::is_utf8($value) ) {
289 16 100       84 return $_ if /^-?\d+$/; # integer (string representation)
290 15 100       74 return $_ if /^-?\d+\.\d+$/; # non-integer
291 14 100       56 return $_ if /^-?\d+(\.\d+)?e[+-]\d\d?$/i;
292             }
293 29         67 s/\\/\\\\/g; # escaped escape
294 29         56 s/^"(.*)"$/$1/; # strip enclosing quotes
295 29         63 s/"/\\"/g; # escape interior quotes
296             }
297 29         96 return qq("$string");
298             }
299              
300              
301             ## no critic ProhibitMultiplePackages
302             package Net::DNS::RR::OPT::NSID; # RFC5001
303              
304             sub _compose {
305 2 100   2   7 my ( undef, @argument ) = map { ref($_) ? %$_ : $_ } @_;
  4         15  
306 2         15 return pack 'H*', pop @argument;
307             }
308              
309 2     2   15 sub _decompose { return pack 'U0a*', unpack 'H*', pop @_ } # mark as UTF-8
310              
311              
312             package Net::DNS::RR::OPT::DAU; # RFC6975
313              
314             sub _compose {
315 6 100   6   14 my ( undef, @argument ) = map { ref($_) ? @$_ : $_ } @_;
  19         49  
316 6         47 return pack 'C*', @argument;
317             }
318              
319 6     6   30 sub _decompose { return [unpack 'C*', pop @_] }
320              
321              
322             package Net::DNS::RR::OPT::DHU; # RFC6975
323             our @ISA = qw(Net::DNS::RR::OPT::DAU);
324              
325             package Net::DNS::RR::OPT::N3U; # RFC6975
326             our @ISA = qw(Net::DNS::RR::OPT::DAU);
327              
328              
329             package Net::DNS::RR::OPT::CLIENT_SUBNET; # RFC7871
330              
331             my %family = qw(1 Net::DNS::RR::A 2 Net::DNS::RR::AAAA);
332             my @field8 = qw(FAMILY SOURCE-PREFIX SCOPE-PREFIX ADDRESS);
333              
334             sub _compose {
335 4     4   9 shift @_;
336 4 100       23 my %argument = ( map( ( $_ => 0 ), @field8 ), map { ref($_) ? %$_ : $_ } @_ );
  9         40  
337 4   100     46 my $family = $family{$argument{FAMILY}} || die 'unrecognised address family';
338 3         6 my $bitmask = $argument{'SOURCE-PREFIX'};
339 3         21 my $address = bless( {}, $family )->address( $argument{ADDRESS} );
340 3         42 return pack 'a* B*', pack( 'nC2', @argument{@field8} ), unpack "B$bitmask", $address;
341             }
342              
343             sub _decompose {
344 4     4   8 my %object;
345 4         31 @object{@field8} = unpack 'nC2a*', pop @_;
346 4   100     41 my $family = $family{$object{FAMILY}} || die 'unrecognised address family';
347 3         11 for ( $object{ADDRESS} ) {
348 3         26 $_ = bless( {address => $_}, $family )->address;
349 3         28 s/:[:0]+$/::/;
350             }
351 3         12 return \%object;
352             }
353              
354              
355             package Net::DNS::RR::OPT::EXPIRE; # RFC7314
356              
357             sub _compose {
358 2 100   2   6 my ( undef, @argument ) = map { ref($_) ? %$_ : $_ } @_;
  4         16  
359 2         17 return pack 'N', pop @argument;
360             }
361              
362             sub _decompose {
363 2     2   6 my $argument = pop @_;
364 2         12 return {'EXPIRE-TIMER' => unpack 'N', $argument};
365             }
366              
367              
368             package Net::DNS::RR::OPT::COOKIE; # RFC7873
369              
370             my @field10 = qw(CLIENT SERVER);
371              
372             sub _compose {
373 3     3   7 my ( undef, @argument ) = @_;
374 3         10 for ( ref( $argument[0] ) ) {
375 3 100       12 /HASH/ && ( @argument = @{$argument[0]}{@field10} );
  1         5  
376 3 100       11 /ARRAY/ && ( @argument = @{$argument[0]} );
  1         4  
377             }
378 3   100     8 return pack 'a8a*', map { pack 'H*', $_ || '' } @argument;
  5         40  
379             }
380              
381             sub _decompose {
382 2     2   5 my %object;
383 2         8 @object{@field10} = map { pack 'U0a*', $_ } unpack 'H16H*', pop @_; # mark as UTF-8
  4         21  
384 2         7 return \%object;
385             }
386              
387              
388             package Net::DNS::RR::OPT::TCP_KEEPALIVE; # RFC7828
389              
390             sub _compose {
391 2 100   2   6 my ( undef, @argument ) = map { ref($_) ? %$_ : $_ } @_;
  4         14  
392 2         16 return pack 'n', pop @argument;
393             }
394              
395             sub _decompose {
396 2     2   8 my $argument = pop @_;
397 2         13 return {'TIMEOUT' => unpack 'n', $argument};
398             }
399              
400              
401             package Net::DNS::RR::OPT::PADDING; # RFC7830
402              
403             sub _compose {
404 5 100   5   17 my ( undef, @argument ) = map { ref($_) ? %$_ : $_ } @_;
  12         41  
405 5   100     21 my $length = pop(@argument) || 0;
406 5         45 return pack "x$length";
407             }
408              
409             sub _decompose {
410 3     3   7 my $argument = pop @_;
411 3 100       30 return {'OPTION-LENGTH' => length $argument} if $argument =~ /^\000*$/;
412 1         7 return {'BASE16' => unpack 'H*', $argument};
413             }
414              
415              
416             package Net::DNS::RR::OPT::CHAIN; # RFC7901
417              
418             sub _compose {
419 1 100   1   4 my ( undef, @argument ) = map { ref($_) ? %$_ : $_ } @_;
  2         10  
420 1         10 return Net::DNS::DomainName->new( pop @argument )->encode;
421             }
422              
423             sub _decompose {
424 2     2   5 my $argument = pop @_;
425 2         18 return {'CLOSEST-TRUST-POINT' => Net::DNS::DomainName->decode( \$argument )->string};
426             }
427              
428              
429             package Net::DNS::RR::OPT::KEY_TAG; # RFC8145
430              
431             sub _compose {
432 2 100   2   6 my ( undef, @argument ) = map { ref($_) ? @$_ : $_ } @_;
  7         21  
433 2         14 return pack 'n*', @argument;
434             }
435              
436 2     2   12 sub _decompose { return [unpack 'n*', pop @_] }
437              
438              
439             package Net::DNS::RR::OPT::EXTENDED_ERROR; # RFC8914
440              
441             sub _compose {
442 6     6   20 my ( undef, @arg ) = @_;
443 6 100       24 my %arg = ref( $arg[0] ) ? %{$arg[0]} : @arg;
  3         13  
444 6   100     51 my $text = join '', Net::DNS::RR::OPT::_JSONify( $arg{'EXTRA-TEXT'} || '' );
445 6         31 return pack 'na*', $arg{'INFO-CODE'}, Net::DNS::Text->new($text)->raw;
446             }
447              
448             sub _decompose {
449 4     4   47 my ( $code, $text ) = unpack 'na*', pop @_;
450 4         18 my $error = $Net::DNS::Parameters::dnserrorbyval{$code};
451 4 100       16 my @error = defined($error) ? ( 'ERROR' => $error ) : ();
452 4         22 my $extra = Net::DNS::Text->decode( \$text, 0, length $text );
453 4         14 for ( $extra->value ) {
454 4 100       56 last unless /^[\[\{]/;
455 2         5 s/([\$\@])/\\$1/g; ## Here be dragons!
456 2         5 my $REGEX = q/("[^"]*"|[\[\]{}:,]|[-0-9.Ee+]+)|\s+|(.)/;
457 2 100       136 my @split = grep { defined && length } split /$REGEX/o;
  67         323  
458 2     1   13 my $value = eval join( ' ', 'no integer;', map { s/^:$/=>/; $_ } @split );
  22     1   42  
  22         256  
  1         11  
  1         3  
  1         9  
  1         102  
  1         5  
  1         6  
459 2 100       69 return {'INFO-CODE' => $code, @error, 'EXTRA-TEXT' => $value} if ref($value);
460             }
461 3         14 return {'INFO-CODE' => $code, @error, 'EXTRA-TEXT' => $extra->value};
462             }
463              
464              
465             package Net::DNS::RR::OPT::REPORT_CHANNEL; # RFC9567
466              
467             sub _compose {
468 2 100   2   5 my ( undef, @argument ) = map { ref($_) ? %$_ : $_ } @_;
  5         19  
469 2         12 return Net::DNS::DomainName->new( pop @argument )->encode;
470             }
471              
472             sub _decompose {
473 2     2   5 my $argument = pop @_;
474 2         14 return {'AGENT-DOMAIN' => Net::DNS::DomainName->decode( \$argument )->string};
475             }
476              
477              
478             package Net::DNS::RR::OPT::ZONEVERSION; # RFC9660
479              
480             my @field19 = qw(LABELCOUNT TYPE VERSION);
481              
482             sub _compose {
483 3     3   30 my ( undef, @argument ) = @_;
484 3         9 for ( ref( $argument[0] ) ) {
485 3 100       15 /HASH/ && ( @argument = @{$argument[0]}{@field19} );
  1         5  
486 3 100       13 /ARRAY/ && ( @argument = @{$argument[0]} );
  2         9  
487             }
488 3 100       24 return scalar(@argument) ? pack( 'C2H*', @argument ) : '';
489             }
490              
491             sub _decompose {
492 8     8   15 my %object;
493 8         33 my ( $l, $t, $v ) = unpack 'C2H*', pop @_;
494 8         44 @object{@field19} = ( $l, $t, pack 'U0a*', $v ); # mark hex data as UTF-8
495 8         21 return \%object;
496             }
497              
498             ########################################
499              
500              
501             1;
502             __END__