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   84 use strict;
  12         21  
  12         401  
4 12     12   68 use warnings;
  12         22  
  12         693  
5             our $VERSION = (qw$Id: OPT.pm 1930 2023-08-21 14:10:10Z willem $)[2];
6              
7 12     12   78 use base qw(Net::DNS::RR);
  12         33  
  12         1015  
8              
9              
10             =head1 NAME
11              
12             Net::DNS::RR::OPT - DNS OPT resource record
13              
14             =cut
15              
16 12     12   85 use integer;
  12         23  
  12         64  
17              
18 12     12   495 use Carp;
  12         31  
  12         912  
19 12     12   82 use Net::DNS::Parameters qw(:rcode :ednsoption);
  12         22  
  12         2377  
20              
21 12     12   96 use constant UTIL => scalar eval { require Scalar::Util; Scalar::Util->can('isdual') };
  12         22  
  12         24  
  12         40  
  12         1151  
22              
23 12     12   81 use constant OPT => Net::DNS::Parameters::typebyname qw(OPT);
  12         31  
  12         48  
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   258 my ( $self, $data, $offset ) = @_;
33              
34 88         283 my $class = delete $self->{class}; # OPT redefines CLASS and TTL fields
35 88 100       473 $self->udpsize($class) if defined $class;
36              
37 88         227 my $ttl = delete $self->{ttl};
38 88 100       469 $self->_ttl($ttl) if defined $ttl;
39              
40 88         232 my $limit = $offset + $self->{rdlength} - 4;
41 88         155 my @index;
42 88         176 eval {
43 88         310 while ( $offset <= $limit ) {
44 22         51 my ( $code, $length ) = unpack "\@$offset nn", $$data;
45 22         54 my $value = unpack "\@$offset x4 a$length", $$data;
46 22         42 $self->{option}{$code} = $value;
47 22         40 push @index, $code;
48 22         44 $offset += $length + 4;
49             }
50             };
51 88         285 @{$self->{index}} = @index;
  88         301  
52 88         281 return;
53             }
54              
55              
56             sub _encode_rdata { ## encode rdata as wire-format octet string
57 100     100   202 my $self = shift;
58              
59 100   100     497 my $option = $self->{option} || {};
60 100         380 return join '', map { pack( 'nna*', $_, length $option->{$_}, $option->{$_} ) } $self->options;
  24         101  
61             }
62              
63              
64             sub encode { ## override RR method
65 99     99 1 276 my $self = shift;
66 99         318 my $data = $self->_encode_rdata;
67 99         331 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 1120 my @line = split /[\r\n]+/, shift->json;
73 5         12 return join '', map {";;$_\n"} @line;
  48         192  
74             }
75              
76             sub class { ## override RR method
77 2     2 1 519 my ( $self, @value ) = @_;
78 2         11 $self->_deprecate(qq[please use "UDPsize()"]);
79 2         6 return $self->udpsize(@value);
80             }
81              
82             sub ttl { ## override RR method
83 2     2 1 775 my ( $self, @value ) = @_;
84 2         7 $self->_deprecate(qq[please use "flags()", "rcode()" or "version()"]);
85 2         5 return $self->_ttl(@value);
86             }
87              
88             sub _ttl {
89 188     188   463 my ( $self, @value ) = @_;
90 188         403 for (@value) {
91 89         543 @{$self}{qw(rcode version flags)} = unpack 'C2n', pack( 'N', $_ );
  89         472  
92 89         321 $self->{rcode} = $self->{rcode} << 4;
93 89         244 return;
94             }
95 99         274 return unpack 'N', pack( 'C2n', $self->rcode >> 4, $self->version, $self->flags );
96             }
97              
98             sub generic { ## override RR method
99 1     1 1 1 my $self = shift;
100 1         3 local $self->{class} = $self->udpsize;
101 1         13 my @xttl = ( $self->rcode >> 4, $self->version, $self->flags );
102 1         5 local $self->{ttl} = unpack 'N', pack( 'C2n', @xttl );
103 1         9 return $self->SUPER::generic;
104             }
105              
106             sub token { ## override RR method
107 1     1 1 2 return grep { !m/^[()]$/ } split /\s+/, &generic;
  7         20  
108             }
109              
110             sub json {
111 5     5 0 6 my $self = shift; # uncoverable pod
112              
113 5         12 my $version = $self->version;
114 5 100       15 unless ( $version == 0 ) {
115 1         2 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         8 my $flags = sprintf '%04x', $self->flags;
124 4         11 my $rcode = $self->rcode;
125 4         8 my $size = $self->udpsize;
126 4         8 my @format = map { join( "\n\t\t\t", $self->_format_option($_) ) } $self->options;
  20         44  
127 4 100       14 my @indent = scalar(@format) ? "\n\t\t" : ();
128 4         16 my @option = join ",\n\t\t", @format;
129              
130 4         85 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 290 my ( $self, @value ) = @_;
143 124         279 for (@value) { $self->{version} = 0 + $_ }
  3         7  
144 124   100     738 return $self->{version} || 0;
145             }
146              
147              
148             sub udpsize {
149 309     309 0 814 my ( $self, @value ) = @_; # uncoverable pod
150 309 100       748 for (@value) { $self->{udpsize} = ( $_ > 512 ) ? $_ : 0 }
  201         757  
151 309   100     1201 return $self->{udpsize} || 0;
152             }
153              
154             sub size {
155 2     2 0 744 my ( $self, @value ) = @_; # uncoverable pod
156 2         9 $self->_deprecate(qq[size() is an alias of "UDPsize()"]);
157 2         6 return $self->udpsize(@value);
158             }
159              
160              
161             sub rcode {
162 679     679 1 1409 my ( $self, @value ) = @_;
163 679 100       1393 for (@value) { $self->{rcode} = ( $_ < 16 ) ? 0 : $_ } # discard non-EDNS rcodes 1 .. 15
  295         894  
164 679   100     3020 return $self->{rcode} || 0;
165             }
166              
167              
168             sub flags {
169 150     150 1 350 my ( $self, @value ) = @_;
170 150         289 for (@value) { $self->{flags} = 0 + $_ }
  12         32  
171 150   100     1361 return $self->{flags} || 0;
172             }
173              
174              
175             sub options {
176 109     109 1 245 my $self = shift;
177 109   100     412 my $option = $self->{option} || {};
178 109 100       636 my @option = defined( $self->{index} ) ? @{$self->{index}} : sort { $a <=> $b } keys %$option;
  4         12  
  183         255  
179 109         512 return @option;
180             }
181              
182             sub option {
183 136     136 1 7182 my ( $self, $name, @value ) = @_;
184 136         305 my $number = ednsoptionbyname($name);
185 136 100       371 return $self->_get_option($number) unless scalar @value;
186 70         176 my $value = $self->_set_option( $number, @value );
187 69 100       284 return $@ ? croak( ( split /\sat/i, $@ )[0] ) : $value;
188             }
189              
190              
191             ########################################
192              
193             sub _get_option {
194 86     86   137 my ( $self, $number ) = @_;
195              
196 86   100     187 my $options = $self->{option} || {};
197 86         143 my $payload = $options->{$number};
198 86 100       216 return $payload unless wantarray;
199 41         79 my $package = join '::', __PACKAGE__, ednsoptionbyval($number);
200 41         128 $package =~ s/-/_/g;
201 41 100       265 if ( $package->can('_decompose') ) {
202 32 100       82 return {'OPTION-LENGTH' => 0} unless length $payload;
203 31         46 my @structure = eval { $package->_decompose($payload) };
  31         67  
204 31 100       146 return @structure if scalar @structure;
205             }
206 10 100       26 warn $@ if $@;
207 10 100       49 return length($payload) ? {BASE16 => unpack 'H*', $payload} : '';
208             }
209              
210              
211             sub _set_option {
212 70     70   135 my ( $self, $number, @value ) = @_;
213 70         99 my ($arg) = @value;
214              
215 70   100     165 my $options = $self->{option} || {};
216 70         116 delete $options->{$number};
217 70 100       157 delete $self->{option} unless scalar( keys %$options );
218              
219 70 100       121 return unless defined $arg;
220 69         108 $self->{option} = $options;
221              
222 69 100       138 if ( ref($arg) eq 'HASH' ) {
223 44         96 for ( keys %$arg ) { $$arg{uc $_} = $$arg{$_} } # tolerate mixed case
  56         115  
224 44         63 my $length = $$arg{'OPTION-LENGTH'};
225 44         61 my $octets = $$arg{'OPTION-DATA'};
226 44 100       115 $octets = pack 'H*', $$arg{'BASE16'} if defined $$arg{'BASE16'};
227 44 100 100     94 $octets = '' if defined($length) && $length == 0;
228 44 100       115 return $options->{$number} = $octets if defined $octets;
229             }
230              
231 38         87 my $option = ednsoptionbyval($number);
232 38         90 my $package = join '::', __PACKAGE__, $option;
233 38         109 $package =~ s/-/_/g;
234 38 100 100     380 return eval { $options->{$number} = $package->_compose(@value) } if length($arg) && $package->can('_compose');
  34         84  
235              
236 4 100       111 croak "unable to compose option $number" if ref($arg);
237 3         10 return $options->{$number} = $arg;
238             }
239              
240              
241             sub _specified {
242 251     251   794 my $self = shift;
243 251         495 return scalar grep { $self->{$_} } qw(udpsize flags rcode option);
  1004         2558  
244             }
245              
246              
247             sub _format_option {
248 20     20   33 my ( $self, $number ) = @_;
249 20         39 my $option = ednsoptionbyval($number);
250 20         42 my ($content) = $self->_get_option($number);
251 20         53 return Net::DNS::RR::_wrap( _JSONify( {$option => $content} ) );
252             }
253              
254              
255             sub _JSONify {
256 89     89   664 my $value = shift;
257 89 100       166 return 'null' unless defined $value;
258              
259 88 100       233 if ( ref($value) eq 'HASH' ) {
260 36         99 my @tags = sort keys %$value;
261 36         61 my $tail = pop @tags;
262 36 100       68 for ( $$value{BASE16} ) { $_ = pack( 'U0a*', $_ ) if defined } # mark as UTF-8
  36         81  
263 36         48 my @body = map { my @x = ( qq("$_":), _JSONify( $$value{$_} ) ); $x[-1] .= ','; @x } @tags;
  5         11  
  5         10  
  5         11  
264 36         90 push @body, ( qq("$tail":), _JSONify( $$value{$tail} ) );
265 36         64 $body[0] = '{' . $body[0];
266 36         51 $body[-1] .= '}';
267 36         116 return @body;
268             }
269              
270 52 100       91 if ( ref($value) eq 'ARRAY' ) {
271 4         8 my @array = @$value;
272 4         6 my @tail = map { _JSONify($_) } grep {defined} pop @array;
  4         7  
  4         10  
273 4         7 my @body = map { my @x = _JSONify($_); $x[-1] .= ','; @x } @array;
  10         12  
  10         14  
  10         19  
274 4         17 return ( '[', @body, @tail, ']' );
275             }
276              
277 48         71 my $string = "$value"; ## stringify, then use isdual() as discriminant
278 48 100       125 return $string if UTIL && Scalar::Util::isdual($value); # native numeric representation
279 26         43 for ($string) {
280 26 100       51 unless ( utf8::is_utf8($value) ) {
281 16 100       69 return $_ if /^-?\d+$/; # integer (string representation)
282 15 100       37 return $_ if /^-?\d+\.\d+$/; # non-integer
283 14 100       41 return $_ if /^-?\d+(\.\d+)?e[+-]\d\d?$/i;
284             }
285 23         43 s/\\/\\\\/g; # escaped escape
286 23         35 s/^"(.*)"$/$1/; # strip enclosing quotes
287 23         36 s/"/\\"/g; # escape interior quotes
288             }
289 23         91 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   5 my ( undef, @argument ) = map { ref($_) ? %$_ : $_ } @_;
  4         15  
298 2         14 return pack 'H*', pop @argument;
299             }
300              
301 2     2   16 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   12 my ( undef, @argument ) = map { ref($_) ? @$_ : $_ } @_;
  19         45  
308 6         34 return pack 'C*', @argument;
309             }
310              
311 6     6   22 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   6 shift @_;
328 4 100       34 my %argument = ( map( ( $_ => 0 ), @field8 ), map { ref($_) ? %$_ : $_ } @_ );
  9         33  
329 4   100     23 my $family = $family{$argument{FAMILY}} || die 'unrecognised address family';
330 3         6 my $bitmask = $argument{'SOURCE-PREFIX'};
331 3         12 my $address = bless( {}, $family )->address( $argument{ADDRESS} );
332 3         33 return pack 'a* B*', pack( 'nC2', @argument{@field8} ), unpack "B$bitmask", $address;
333             }
334              
335             sub _decompose {
336 4     4   8 my %object;
337 4         25 @object{@field8} = unpack 'nC2a*', pop @_;
338 4   100     22 my $family = $family{$object{FAMILY}} || die 'unrecognised address family';
339 3         6 for ( $object{ADDRESS} ) {
340 3         16 $_ = bless( {address => $_}, $family )->address;
341 3         21 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   4 my ( undef, @argument ) = map { ref($_) ? %$_ : $_ } @_;
  4         82  
351 2         13 return pack 'N', pop @argument;
352             }
353              
354             sub _decompose {
355 2     2   78 my $argument = pop @_;
356 2         15 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         7 for ( ref( $argument[0] ) ) {
367 3 100       9 /HASH/ && ( @argument = @{$argument[0]}{@field10} );
  1         4  
368 3 100       7 /ARRAY/ && ( @argument = @{$argument[0]} );
  1         3  
369             }
370 3   100     6 return pack 'a8a*', map { pack 'H*', $_ || '' } @argument;
  5         32  
371             }
372              
373             sub _decompose {
374 2     2   3 my %object;
375 2         25 @object{@field10} = map { pack 'U0a*', $_ } unpack 'H16H*', pop @_; # mark as UTF-8
  4         23  
376 2         6 return \%object;
377             }
378              
379              
380             package Net::DNS::RR::OPT::TCP_KEEPALIVE; # RFC7828
381              
382             sub _compose {
383 2 100   2   3 my ( undef, @argument ) = map { ref($_) ? %$_ : $_ } @_;
  4         14  
384 2         12 return pack 'n', pop @argument;
385             }
386              
387             sub _decompose {
388 2     2   5 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   14 my ( undef, @argument ) = map { ref($_) ? %$_ : $_ } @_;
  12         39  
397 5   100     18 my $length = pop(@argument) || 0;
398 5         37 return pack "x$length";
399             }
400              
401             sub _decompose {
402 3     3   7 my $argument = pop @_;
403 3 100       17 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         10  
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         9 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   3 my ( undef, @argument ) = map { ref($_) ? @$_ : $_ } @_;
  7         18  
425 2         12 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   13 my ( undef, @arg ) = @_;
435 6 100       18 my %arg = ref( $arg[0] ) ? %{$arg[0]} : @arg;
  3         12  
436 6   100     22 my $text = join '', Net::DNS::RR::OPT::_JSONify( $arg{'EXTRA-TEXT'} || '' );
437 6         29 return pack 'na*', $arg{'INFO-CODE'}, Net::DNS::Text->new($text)->raw;
438             }
439              
440             sub _decompose {
441 4     4   14 my ( $code, $text ) = unpack 'na*', pop @_;
442 4         11 my $error = $Net::DNS::Parameters::dnserrorbyval{$code};
443 4 100       12 my @error = defined($error) ? ( 'ERROR' => $error ) : ();
444 4         15 my $extra = Net::DNS::Text->decode( \$text, 0, length $text );
445 4         10 for ( $extra->value ) {
446 4 100       49 last unless /^[\[\{]/;
447 2         6 s/([\$\@])/\\$1/g; ## Here be dragons!
448 2         4 my $REGEX = q/("[^"]*"|[\[\]{}:,]|[-0-9.Ee+]+)|\s+|(.)/;
449 2 100       86 my @split = grep { defined && length } split /$REGEX/o;
  67         144  
450 2     1   7 my $value = eval join( ' ', 'no integer;', map { s/^:$/=>/; $_ } @split );
  22     1   34  
  22         188  
  1         30  
  1         3  
  1         4  
  1         7  
  1         1  
  1         5  
451 2 100       54 return {'INFO-CODE' => $code, @error, 'EXTRA-TEXT' => $value} if ref($value);
452             }
453 3         11 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             $Net::DNS::Parameters::ednsoptionbyval{65023} = 'REPORT-CHANNEL'; ## experimental/private use
459              
460             sub _compose {
461 1 100   1   3 my ( undef, @argument ) = map { ref($_) ? %$_ : $_ } @_;
  2         8  
462 1         4 return Net::DNS::DomainName->new( pop @argument )->encode;
463             }
464              
465             sub _decompose {
466 2     2   5 my $argument = pop @_;
467 2         14 return {'AGENT-DOMAIN' => Net::DNS::DomainName->decode( \$argument )->string};
468             }
469              
470             ########################################
471              
472              
473             1;
474             __END__