File Coverage

blib/lib/Net/DNS/RR/OPT.pm
Criterion Covered Total %
statement 285 285 100.0
branch 98 98 100.0
condition 32 32 100.0
subroutine 54 54 100.0
pod 11 14 100.0
total 480 483 100.0


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