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   87 use strict;
  12         26  
  12         429  
4 12     12   72 use warnings;
  12         24  
  12         711  
5             our $VERSION = (qw$Id: OPT.pm 1934 2023-08-25 12:14:08Z willem $)[2];
6              
7 12     12   92 use base qw(Net::DNS::RR);
  12         31  
  12         1052  
8              
9              
10             =head1 NAME
11              
12             Net::DNS::RR::OPT - DNS OPT resource record
13              
14             =cut
15              
16 12     12   88 use integer;
  12         27  
  12         58  
17              
18 12     12   438 use Carp;
  12         29  
  12         955  
19 12     12   89 use Net::DNS::Parameters qw(:rcode :ednsoption);
  12         32  
  12         2419  
20              
21 12     12   103 use constant UTIL => scalar eval { require Scalar::Util; Scalar::Util->can('isdual') };
  12         25  
  12         16  
  12         69  
  12         1188  
22              
23 12     12   91 use constant OPT => Net::DNS::Parameters::typebyname qw(OPT);
  12         25  
  12         51  
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 76     76   273 my ( $self, $data, $offset ) = @_;
33              
34 76         271 my $class = delete $self->{class}; # OPT redefines CLASS and TTL fields
35 76 100       504 $self->udpsize($class) if defined $class;
36              
37 76         235 my $ttl = delete $self->{ttl};
38 76 100       519 $self->_ttl($ttl) if defined $ttl;
39              
40 76         216 my $limit = $offset + $self->{rdlength} - 4;
41 76         193 my @index;
42 76         156 eval {
43 76         334 while ( $offset <= $limit ) {
44 22         52 my ( $code, $length ) = unpack "\@$offset nn", $$data;
45 22         56 my $value = unpack "\@$offset x4 a$length", $$data;
46 22         46 $self->{option}{$code} = $value;
47 22         39 push @index, $code;
48 22         45 $offset += $length + 4;
49             }
50             };
51 76         161 @{$self->{index}} = @index;
  76         304  
52 76         260 return;
53             }
54              
55              
56             sub _encode_rdata { ## encode rdata as wire-format octet string
57 88     88   178 my $self = shift;
58              
59 88   100     482 my $option = $self->{option} || {};
60 88         402 return join '', map { pack( 'nna*', $_, length $option->{$_}, $option->{$_} ) } $self->options;
  24         143  
61             }
62              
63              
64             sub encode { ## override RR method
65 87     87 1 267 my $self = shift;
66 87         346 my $data = $self->_encode_rdata;
67 87         412 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 1449 my @line = split /[\r\n]+/, shift->json;
73 5         13 return join '', map {";;$_\n"} @line;
  48         1144  
74             }
75              
76             sub class { ## override RR method
77 2     2 1 671 my ( $self, @value ) = @_;
78 2         12 $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 1030 my ( $self, @value ) = @_;
84 2         40 $self->_deprecate(qq[please use "flags()", "rcode()" or "version()"]);
85 2         6 return $self->_ttl(@value);
86             }
87              
88             sub _ttl {
89 164     164   403 my ( $self, @value ) = @_;
90 164         426 for (@value) {
91 77         561 @{$self}{qw(rcode version flags)} = unpack 'C2n', pack( 'N', $_ );
  77         481  
92 77         307 $self->{rcode} = $self->{rcode} << 4;
93 77         204 return;
94             }
95 87         315 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         6 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         25  
108             }
109              
110             sub json {
111 5     5 0 17 my $self = shift; # uncoverable pod
112              
113 5         10 my $version = $self->version;
114 5 100       17 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         8 my $flags = sprintf '%04x', $self->flags;
124 4         16 my $rcode = $self->rcode;
125 4         9 my $size = $self->udpsize;
126 4         9 my @format = map { join( "\n\t\t\t", $self->_format_option($_) ) } $self->options;
  20         48  
127 4 100       15 my @indent = scalar(@format) ? "\n\t\t" : ();
128 4         15 my @option = join ",\n\t\t", @format;
129              
130 4         90 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 112     112 1 333 my ( $self, @value ) = @_;
143 112         249 for (@value) { $self->{version} = 0 + $_ }
  3         7  
144 112   100     638 return $self->{version} || 0;
145             }
146              
147              
148             sub udpsize {
149 281     281 0 786 my ( $self, @value ) = @_; # uncoverable pod
150 281 100       661 for (@value) { $self->{udpsize} = ( $_ > 512 ) ? $_ : 0 }
  185         866  
151 281   100     1163 return $self->{udpsize} || 0;
152             }
153              
154             sub size {
155 2     2 0 1068 my ( $self, @value ) = @_; # uncoverable pod
156 2         8 $self->_deprecate(qq[size() is an alias of "UDPsize()"]);
157 2         6 return $self->udpsize(@value);
158             }
159              
160              
161             sub rcode {
162 623     623 1 1330 my ( $self, @value ) = @_;
163 623 100       1203 for (@value) { $self->{rcode} = ( $_ < 16 ) ? 0 : $_ } # discard non-EDNS rcodes 1 .. 15
  273         943  
164 623   100     2932 return $self->{rcode} || 0;
165             }
166              
167              
168             sub flags {
169 138     138 1 392 my ( $self, @value ) = @_;
170 138         308 for (@value) { $self->{flags} = 0 + $_ }
  12         38  
171 138   100     1308 return $self->{flags} || 0;
172             }
173              
174              
175             sub options {
176 97     97 1 216 my $self = shift;
177 97   100     433 my $option = $self->{option} || {};
178 97 100       683 my @option = defined( $self->{index} ) ? @{$self->{index}} : sort { $a <=> $b } keys %$option;
  4         10  
  204         260  
179 97         515 return @option;
180             }
181              
182             sub option {
183 136     136 1 9317 my ( $self, $name, @value ) = @_;
184 136         322 my $number = ednsoptionbyname($name);
185 136 100       333 return $self->_get_option($number) unless scalar @value;
186 70         160 my $value = $self->_set_option( $number, @value );
187 69 100       301 return $@ ? croak( ( split /\sat/i, $@ )[0] ) : $value;
188             }
189              
190              
191             ########################################
192              
193             sub _get_option {
194 86     86   148 my ( $self, $number ) = @_;
195              
196 86   100     178 my $options = $self->{option} || {};
197 86         134 my $payload = $options->{$number};
198 86 100       223 return $payload unless wantarray;
199 41         86 my $package = join '::', __PACKAGE__, ednsoptionbyval($number);
200 41         129 $package =~ s/-/_/g;
201 41 100       286 if ( $package->can('_decompose') ) {
202 32 100       71 return {'OPTION-LENGTH' => 0} unless length $payload;
203 31         43 my @structure = eval { $package->_decompose($payload) };
  31         76  
204 31 100       150 return @structure if scalar @structure;
205             }
206 10 100       28 warn $@ if $@;
207 10 100       52 return length($payload) ? {BASE16 => unpack 'H*', $payload} : '';
208             }
209              
210              
211             sub _set_option {
212 70     70   135 my ( $self, $number, @value ) = @_;
213 70         108 my ($arg) = @value;
214              
215 70   100     165 my $options = $self->{option} || {};
216 70         127 delete $options->{$number};
217 70 100       167 delete $self->{option} unless scalar( keys %$options );
218              
219 70 100       164 return unless defined $arg;
220 69         97 $self->{option} = $options;
221              
222 69 100       143 if ( ref($arg) eq 'HASH' ) {
223 44         97 for ( keys %$arg ) { $$arg{uc $_} = $$arg{$_} } # tolerate mixed case
  56         163  
224 44         66 my $length = $$arg{'OPTION-LENGTH'};
225 44         65 my $octets = $$arg{'OPTION-DATA'};
226 44 100       111 $octets = pack 'H*', $$arg{'BASE16'} if defined $$arg{'BASE16'};
227 44 100 100     96 $octets = '' if defined($length) && $length == 0;
228 44 100       126 return $options->{$number} = $octets if defined $octets;
229             }
230              
231 38         90 my $option = ednsoptionbyval($number);
232 38         95 my $package = join '::', __PACKAGE__, $option;
233 38         119 $package =~ s/-/_/g;
234 38 100 100     380 return eval { $options->{$number} = $package->_compose(@value) } if length($arg) && $package->can('_compose');
  34         85  
235              
236 4 100       110 croak "unable to compose option $number" if ref($arg);
237 3         20 return $options->{$number} = $arg;
238             }
239              
240              
241             sub _specified {
242 239     239   929 my $self = shift;
243 239         521 return scalar grep { $self->{$_} } qw(udpsize flags rcode option);
  956         2416  
244             }
245              
246              
247             sub _format_option {
248 20     20   34 my ( $self, $number ) = @_;
249 20         41 my $option = ednsoptionbyval($number);
250 20         41 my ($content) = $self->_get_option($number);
251 20         60 return Net::DNS::RR::_wrap( _JSONify( {$option => $content} ) );
252             }
253              
254              
255             sub _JSONify {
256 89     89   779 my $value = shift;
257 89 100       161 return 'null' unless defined $value;
258              
259 88 100       163 if ( ref($value) eq 'HASH' ) {
260 36         102 my @tags = sort keys %$value;
261 36         58 my $tail = pop @tags;
262 36 100       70 for ( $$value{BASE16} ) { $_ = pack( 'U0a*', $_ ) if defined } # mark as UTF-8
  36         83  
263 36         49 my @body = map { my @x = ( qq("$_":), _JSONify( $$value{$_} ) ); $x[-1] .= ','; @x } @tags;
  5         11  
  5         10  
  5         13  
264 36         93 push @body, ( qq("$tail":), _JSONify( $$value{$tail} ) );
265 36         67 $body[0] = '{' . $body[0];
266 36         53 $body[-1] .= '}';
267 36         119 return @body;
268             }
269              
270 52 100       95 if ( ref($value) eq 'ARRAY' ) {
271 4         8 my @array = @$value;
272 4         8 my @tail = map { _JSONify($_) } grep {defined} pop @array;
  4         7  
  4         11  
273 4         5 my @body = map { my @x = _JSONify($_); $x[-1] .= ','; @x } @array;
  10         15  
  10         15  
  10         21  
274 4         36 return ( '[', @body, @tail, ']' );
275             }
276              
277 48         68 my $string = "$value"; ## stringify, then use isdual() as discriminant
278 48 100       130 return $string if UTIL && Scalar::Util::isdual($value); # native numeric representation
279 26         42 for ($string) {
280 26 100       54 unless ( utf8::is_utf8($value) ) {
281 16 100       64 return $_ if /^-?\d+$/; # integer (string representation)
282 15 100       43 return $_ if /^-?\d+\.\d+$/; # non-integer
283 14 100       40 return $_ if /^-?\d+(\.\d+)?e[+-]\d\d?$/i;
284             }
285 23         40 s/\\/\\\\/g; # escaped escape
286 23         37 s/^"(.*)"$/$1/; # strip enclosing quotes
287 23         40 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   5 my ( undef, @argument ) = map { ref($_) ? %$_ : $_ } @_;
  4         15  
298 2         21 return pack 'H*', pop @argument;
299             }
300              
301 2     2   13 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         47  
308 6         33 return pack 'C*', @argument;
309             }
310              
311 6     6   63 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   4 shift @_;
328 4 100       30 my %argument = ( map( ( $_ => 0 ), @field8 ), map { ref($_) ? %$_ : $_ } @_ );
  9         33  
329 4   100     22 my $family = $family{$argument{FAMILY}} || die 'unrecognised address family';
330 3         6 my $bitmask = $argument{'SOURCE-PREFIX'};
331 3         14 my $address = bless( {}, $family )->address( $argument{ADDRESS} );
332 3         32 return pack 'a* B*', pack( 'nC2', @argument{@field8} ), unpack "B$bitmask", $address;
333             }
334              
335             sub _decompose {
336 4     4   10 my %object;
337 4         24 @object{@field8} = unpack 'nC2a*', pop @_;
338 4   100     22 my $family = $family{$object{FAMILY}} || die 'unrecognised address family';
339 3         7 for ( $object{ADDRESS} ) {
340 3         15 $_ = bless( {address => $_}, $family )->address;
341 3         19 s/:[:0]+$/::/;
342             }
343 3         12 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         102 return pack 'N', pop @argument;
352             }
353              
354             sub _decompose {
355 2     2   4 my $argument = pop @_;
356 2         98 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         8 for ( ref( $argument[0] ) ) {
367 3 100       9 /HASH/ && ( @argument = @{$argument[0]}{@field10} );
  1         4  
368 3 100       8 /ARRAY/ && ( @argument = @{$argument[0]} );
  1         4  
369             }
370 3   100     6 return pack 'a8a*', map { pack 'H*', $_ || '' } @argument;
  5         35  
371             }
372              
373             sub _decompose {
374 2     2   3 my %object;
375 2         7 @object{@field10} = map { pack 'U0a*', $_ } unpack 'H16H*', pop @_; # mark as UTF-8
  4         24  
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   5 my ( undef, @argument ) = map { ref($_) ? %$_ : $_ } @_;
  4         15  
384 2         12 return pack 'n', pop @argument;
385             }
386              
387             sub _decompose {
388 2     2   4 my $argument = pop @_;
389 2         13 return {'TIMEOUT' => unpack 'n', $argument};
390             }
391              
392              
393             package Net::DNS::RR::OPT::PADDING; # RFC7830
394              
395             sub _compose {
396 5 100   5   15 my ( undef, @argument ) = map { ref($_) ? %$_ : $_ } @_;
  12         53  
397 5   100     23 my $length = pop(@argument) || 0;
398 5         41 return pack "x$length";
399             }
400              
401             sub _decompose {
402 3     3   9 my $argument = pop @_;
403 3 100       23 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   12 my ( undef, @argument ) = map { ref($_) ? %$_ : $_ } @_;
  2         8  
412 1         7 return Net::DNS::DomainName->new( pop @argument )->encode;
413             }
414              
415             sub _decompose {
416 2     2   16 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   3 my ( undef, @argument ) = map { ref($_) ? @$_ : $_ } @_;
  7         17  
425 2         12 return pack 'n*', @argument;
426             }
427              
428 2     2   9 sub _decompose { return [unpack 'n*', pop @_] }
429              
430              
431             package Net::DNS::RR::OPT::EXTENDED_ERROR; # RFC8914
432              
433             sub _compose {
434 6     6   14 my ( undef, @arg ) = @_;
435 6 100       17 my %arg = ref( $arg[0] ) ? %{$arg[0]} : @arg;
  3         14  
436 6   100     24 my $text = join '', Net::DNS::RR::OPT::_JSONify( $arg{'EXTRA-TEXT'} || '' );
437 6         23 return pack 'na*', $arg{'INFO-CODE'}, Net::DNS::Text->new($text)->raw;
438             }
439              
440             sub _decompose {
441 4     4   15 my ( $code, $text ) = unpack 'na*', pop @_;
442 4         17 my $error = $Net::DNS::Parameters::dnserrorbyval{$code};
443 4 100       10 my @error = defined($error) ? ( 'ERROR' => $error ) : ();
444 4         15 my $extra = Net::DNS::Text->decode( \$text, 0, length $text );
445 4         12 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       95 my @split = grep { defined && length } split /$REGEX/o;
  67         150  
450 2     1   9 my $value = eval join( ' ', 'no integer;', map { s/^:$/=>/; $_ } @split );
  22     1   33  
  22         201  
  1         17  
  1         2  
  1         9  
  1         8  
  1         2  
  1         3  
451 2 100       62 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              
459             sub _compose {
460 1 100   1   3 my ( undef, @argument ) = map { ref($_) ? %$_ : $_ } @_;
  2         14  
461 1         4 return Net::DNS::DomainName->new( pop @argument )->encode;
462             }
463              
464             sub _decompose {
465 2     2   15 my $argument = pop @_;
466 2         9 return {'AGENT-DOMAIN' => Net::DNS::DomainName->decode( \$argument )->string};
467             }
468              
469             ########################################
470              
471              
472             1;
473             __END__