File Coverage

blib/lib/Zonemaster/Engine/Config.pm
Criterion Covered Total %
statement 123 132 93.1
branch 27 36 75.0
condition 6 9 66.6
subroutine 27 27 100.0
pod 14 14 100.0
total 197 218 90.3


line stmt bran cond sub pod time code
1             package Zonemaster::Engine::Config;
2              
3 26     26   67309 use version; our $VERSION = version->declare("v1.0.6");
  26         2030  
  26         197  
4              
5 26     26   2943 use 5.014002;
  26         105  
6 26     26   140 use warnings;
  26         52  
  26         785  
7              
8 26     26   559 use Moose;
  26         521924  
  26         198  
9 26     26   162187 use JSON::PP;
  26         11196  
  26         1908  
10 26     26   2065 use File::ShareDir qw[dist_dir dist_file];
  26         24402  
  26         1483  
11 26     26   9971 use File::Slurp;
  26         160831  
  26         1673  
12 26     26   8361 use Hash::Merge;
  26         52527  
  26         1110  
13 26     26   188 use File::Spec;
  26         61  
  26         616  
14              
15 26     26   473 use Zonemaster::Engine;
  26         60  
  26         30764  
16              
17             has 'cfiles' => ( is => 'ro', isa => 'ArrayRef', default => sub { [] } );
18             has 'pfiles' => ( is => 'ro', isa => 'ArrayRef', default => sub { [] } );
19             has 'testcases' => ( is => 'ro', isa => 'HashRef', default => sub { {} } );
20              
21             my $merger = Hash::Merge->new;
22             $merger->specify_behavior(
23             {
24             'SCALAR' => {
25             'SCALAR' => sub { $_[1] },
26             'ARRAY' => sub { [ $_[0], @{ $_[1] } ] },
27             'HASH' => sub { $_[1] },
28             },
29             'ARRAY' => {
30             'SCALAR' => sub { $_[1] },
31             'ARRAY' => sub { [ @{ $_[1] } ] },
32             'HASH' => sub { $_[1] },
33             },
34             'HASH' => {
35             'SCALAR' => sub { $_[1] },
36             'ARRAY' => sub { [ values %{ $_[0] }, @{ $_[1] } ] },
37             'HASH' => sub { Hash::Merge::_merge_hashes( $_[0], $_[1] ) },
38             },
39             }
40             );
41              
42             our $config;
43             _load_base_config();
44              
45             our $policy = {};
46              
47             sub BUILD {
48 24     24 1 63 my ( $self ) = @_;
49              
50 24         95 foreach my $dir ( _config_directory_list() ) {
51 96         1348 my $cfile = File::Spec->catfile( $dir, 'config.json' );
52 96         264 my $new = eval { decode_json scalar read_file $cfile };
  96         456  
53 96 100       87692 if ( $new ) {
54 24         196 $config = $merger->merge( $config, $new );
55 24         488 push @{ $self->cfiles }, $cfile;
  24         993  
56             }
57              
58 96         1074 my $pfile = File::Spec->catfile( $dir, 'policy.json' );
59 96         260 $new = eval { decode_json scalar read_file $pfile };
  96         324  
60 96 100       1530018 if ( $new ) {
61 24         100 my $tc = $new->{__testcases__};
62 24         75 delete $new->{__testcases__};
63 24         65 foreach my $case ( keys %{$tc} ) {
  24         401  
64 1368         35292 $self->testcases->{$case} = $tc->{$case};
65             }
66 24         272 $policy = $merger->merge( $policy, $new );
67 24         1814 push @{ $self->pfiles }, $pfile;
  24         891  
68             }
69              
70             } ## end foreach my $dir ( _config_directory_list...)
71              
72 24         881 return $self;
73             } ## end sub BUILD
74              
75             sub get {
76 136306     136306 1 226834 my ( $class ) = @_;
77              
78 136306         601805 return $config;
79             }
80              
81             sub policy {
82 29     29 1 68 my ( $class ) = @_;
83              
84 29 50       94 if ( not $policy ) {
85 0         0 _load_base_policy();
86             }
87              
88 29         497 return $policy;
89             }
90              
91             sub _config_directory_list {
92 24     24   49 my @dirlist;
93 24         64 my $makefile_name = 'Zonemaster-Engine'; # This must be the same name as "name" in Makefile.PL
94 24         151 push @dirlist, dist_dir( $makefile_name );
95 24         2154 push @dirlist, '/etc/zonemaster';
96 24         69 push @dirlist, '/usr/local/etc/zonemaster';
97              
98 24         11832 my $dir = ( getpwuid( $> ) )[7];
99 24 50       173 if ( $dir ) {
100 24         104 push @dirlist, $dir . '/.zonemaster';
101             }
102              
103 24         124 return @dirlist;
104             }
105              
106             sub _load_base_config {
107 26     26   670 my $internal = decode_json( join( q{}, <DATA> ) );
108             # my $filename = dist_file( 'Zonemaster', 'config.json' );
109             # my $default = eval { decode_json scalar read_file $filename };
110             #
111             # $internal = $merger->merge( $internal, $default ) if $default;
112              
113 26         71397 $config = $internal;
114              
115 26         76 return;
116             }
117              
118             sub load_module_policy {
119 218     218 1 682 my ( $class, $mod ) = @_;
120              
121 218         632 my $m = 'Zonemaster::Engine::Test::' . $mod;
122 218 100 66     2745 if ( $m->can( 'policy' ) and $m->policy ) {
123 76         268 $policy = $merger->merge( $policy, { $mod => $m->policy } );
124             }
125              
126 218         8896 return;
127             }
128              
129             sub load_config_file {
130 2     2 1 8 my ( $self, $filename ) = @_;
131 2         12 my $new = decode_json scalar read_file $filename;
132              
133 2 50       7950 if ( $new ) {
134 2         21 $config = $merger->merge( $config, $new );
135 2 100 66     71 push @{ $self->cfiles }, $filename if ( ref( $self ) and $self->isa( __PACKAGE__ ) );
  1         45  
136             }
137              
138 2         17 return !!$new;
139             }
140              
141             sub load_policy_file {
142 31     31 1 97 my ( $self, $filename ) = @_;
143              
144 31 50       1026 if ( not -r $filename ) {
145 0         0 foreach my $dir ( _config_directory_list() ) {
146 0         0 my $name = File::Spec->catfile( $dir, $filename );
147 0 0       0 if ( -r $name ) {
148 0         0 $filename = $name;
149 0         0 last;
150             }
151             else {
152 0 0       0 if ( -r $name . '.json' ) {
153 0         0 $filename = $name . '.json';
154 0         0 last;
155             }
156             }
157             }
158             }
159              
160 31         191 my $new = decode_json scalar read_file $filename;
161 31 50       54625 if ( $new ) {
162 31         93 my $tc = $new->{__testcases__};
163 31         74 delete $new->{__testcases__};
164 31         63 foreach my $case ( keys %{$tc} ) {
  31         168  
165 229         5615 $self->testcases->{$case} = $tc->{$case};
166             }
167 31         224 $policy = $merger->merge( $policy, $new );
168 31 100 66     2469 push @{ $self->pfiles }, $filename if ( ref( $self ) and $self->isa( __PACKAGE__ ) );
  30         985  
169             }
170              
171 31         117 return !!$new;
172             } ## end sub load_policy_file
173              
174             sub no_network {
175 52     52 1 157 my ( $class, $value ) = @_;
176              
177 52 100       183 if ( defined( $value ) ) {
178 32         109 $class->get->{no_network} = $value;
179             }
180              
181 52         155 return $class->get->{no_network};
182             }
183              
184             sub ipv4_ok {
185 11058     11058 1 30473 my ( $class, $value ) = @_;
186              
187 11058 100       35165 if ( defined( $value ) ) {
188 14         45 $class->get->{net}{ipv4} = $value;
189             }
190              
191 11058         28970 return $class->get->{net}{ipv4};
192             }
193              
194             sub ipv6_ok {
195 14468     14468 1 39266 my ( $class, $value ) = @_;
196              
197 14468 100       39721 if ( defined( $value ) ) {
198 14         70 $class->get->{net}{ipv6} = $value;
199             }
200              
201 14468         38063 return $class->get->{net}{ipv6};
202             }
203              
204             sub resolver_defaults {
205 21418     21418 1 55729 my ( $class ) = @_;
206              
207 21418         61282 return $class->get->{resolver}{defaults};
208             }
209              
210             sub resolver_source {
211 7     7 1 26 my ( $class, $sourceaddr ) = @_;
212              
213 7 100       27 if ( defined( $sourceaddr ) ) {
214 2         10 $class->get->{resolver}{source} = $sourceaddr;
215             }
216              
217 7         27 return $class->get->{resolver}{source};
218             }
219              
220             sub logfilter {
221 89236     89236 1 170978 my ( $class ) = @_;
222              
223 89236         224024 return $class->get->{logfilter};
224             }
225              
226             sub asnroots {
227 3     3 1 12 my ( $class ) = @_;
228              
229 3         11 return $class->get->{asnroots};
230             }
231              
232             sub should_run {
233 409     409 1 1001 my ( $self, $name ) = @_;
234              
235 409 100       11249 if ( not defined $self->testcases->{$name} ) {
    100          
236 1         4 return 1; # Default to runnings test
237             }
238             elsif ( $self->testcases->{$name} ) {
239 244         2939 return 1;
240             }
241             else {
242 164         587 return;
243             }
244             }
245              
246 26     26   209 no Moose;
  26         57  
  26         216  
247             __PACKAGE__->meta->make_immutable;
248              
249             1;
250              
251             =head1 NAME
252              
253             Zonemaster::Engine::Config - configuration access module for Zonemaster::Engine
254              
255             =head1 SYNOPSIS
256              
257             Zonemaster::Engine->config->no_network(1); # Forbid network traffic
258              
259             my $value = Zonemaster::Engine::Config->get->{key}{subkey}; # Not really recommended way to access config data
260              
261             =head1 LOADING CONFIGURATION
262              
263             Configuration data is loaded in several stages, each one overlaying the result
264             from the previous one (that is, the later in the list take priority over the
265             earlier). The first stage is hardcoded into the source code and loaded while it
266             is being compiled, to make sure that there will always be some basic
267             information available. Later, when the configuration object is first used, the
268             system will look for a file named F<config.json> in each of a list of
269             directories. If the file exists, is readable and contains proper JSON data, it
270             will be loaded and overlaid on the current internal config. The directories
271             are, in order from first checked to last:
272              
273             =over
274              
275             =item The L<Zonemaster::Engine> perl module installation directory
276              
277             This is where the installation process puts the default configuration. It is
278             not meant to be modified by the user, and it will be overwritten when the
279             module is upgraded (or reinstalled for any other reason). If you really need to
280             know where it is, you can either check the log message left when loading it or
281             run this command to find the path:
282              
283             perl -MFile::ShareDir=dist_dir -E 'say dist_dir( "Zonemaster" )'
284              
285             =item /etc/zonemaster
286              
287             Intended to hold system-global configuration changes.
288              
289             =item /usr/local/etc/zonemaster
290              
291             Basically the same as the previous one, but for those who like to keep their
292             locally installed software inside F</usr/local>.
293              
294             =item ~/.zonemaster
295              
296             That is, a F<.zonemaster> directory in the home directory of the current user.
297             Intended, obviously, for configuration changes local to one particular user.
298              
299             =back
300              
301             The possible contents of the JSON data is described further down in this manual
302             page.
303              
304             =head1 METHODS FOR CONFIGURATION ITEMS
305              
306             =over
307              
308             =item no_network([$value])
309              
310             Returns the value of the C<no_network> flag. If given a defined value, sets the value to that value.
311              
312             =item ipv4_ok([$value])
313              
314             Returns the value of the C<ipv4> flag. If given a defined value, sets the value to that value.
315              
316             =item ipv6_ok([$value])
317              
318             Returns the value of the C<ipv6> flag. If given a defined value, sets the value to that value.
319              
320             =item resolver_defaults()
321              
322             Returns a reference to the resolver_defaults hash.
323              
324             =item resolver_source([$addr])
325              
326             Returns the source address all resolver objects should use when sending
327             queries, if one is set. If given an argument, sets the source address to the
328             argument.
329              
330             =item logfilter()
331              
332             Returns a reference to the logfilter hash.
333              
334             =item asnroots()
335              
336             Returns a reference to the list of ASN lookup domains.
337              
338             =back
339              
340             =head1 METHODS
341              
342             =over
343              
344             =item get()
345              
346             Returns a reference to a hash with configuration values.
347              
348             =item policy()
349              
350             Returns a reference to the current policy data. The format of that data is described further down in this document.
351              
352             =item load_policy_file($filename)
353              
354             Load policy information from the given file and merge it into the pre-loaded
355             policy. Information from the loaded file overrides the pre-loaded information
356             when the same keys exist in both places.
357              
358             If the given name does not lead directly to a readable file, each of the usual
359             directories will be checked if the name is there. If the plain name isn't, the
360             suffix C<.json> will be appended and another try will be done. For example, a
361             file F<$HOME/.zonemaster/Example.json> may be loaded by calling this method
362             with the string C<"Example">.
363              
364             =item load_config_file($filename)
365              
366             Load configuration information from the given file and merge it into the pre-loaded config. Information from the loaded file overrides the pre-loaded information when the same keys exist in both places.
367              
368             =item load_module_policy($module)
369              
370             Loads policy data included in a test module. The argument must be the short
371             form (without the initial C<Zonemaster::Engine::Test::>) and correctly capitalized.
372              
373             =item BUILD
374              
375             Internal method only mentioned here to please L<Pod::Coverage>.
376              
377             =item should_run($name)
378              
379             Given a test case name, it returns true if that test case should be included in
380             a test run according to the currently active policy or false if not.
381              
382             =back
383              
384             =head1 CONFIGURATION DATA
385              
386             The configuration data is stored internally in a nested hash (possibly with arrays as values in places). As of this writing, the file format used is JSON.
387              
388             The interesting keys are as follows.
389              
390             =head2 resolver
391              
392             =head3 defaults
393              
394             These are the default flag and timing values used for the resolver objects used to actually send DNS queries.
395              
396             =over
397              
398             =item usevc
399              
400             If set, only use TCP. Default not set.
401              
402             =item retrans
403              
404             The number of seconds between retries. Default 3.
405              
406             =item dnssec
407              
408             If set, sets the DO flag in queries. Default not set.
409              
410             =item recurse
411              
412             If set, sets the RD flag in queries. Default not set (and almost certainly should remain that way).
413              
414             =item retry
415              
416             The number of times a query is sent before we give up. Can be set to zero, although that's not very useful (since no queries will be sent at all). Defaults to 2.
417              
418             =item igntc
419              
420             If set, queries that get truncated UDP responses will be automatically retried over TCP. Default not set.
421              
422             =back
423              
424             =head2 net
425              
426             =over
427              
428             =item ipv4
429              
430             If set, resolver objects are allowed to send queries over IPv4. Default set.
431              
432             =item ipv6
433              
434             If set, resolver objects are allowed to send queries over IPv6. Default set.
435              
436             =back
437              
438             =head2 no_network
439              
440             If set to a true value, network traffic is forbidden. Use when you want to be sure that any data is only taken from a preloaded cache.
441              
442             =head2 asnroots
443              
444             This key must be a list of domain names. The domains will be assumed to be
445             Cymru-style AS lookup zones. Normally only the first name in the list will be
446             used, the rest are backups in case the earlier ones don't work.
447              
448             =head2 logfilter
449              
450             By using this key, the log level of messages can be set in a much more fine-grained way than by the policy file. The intended use is to remove known erroneous results. If you, for example, know that a certain name server is recursive and for some reason should be, you can use this functionality to lower the severity of the complaint about it to a lower level than normal.
451              
452             The data under the C<logfilter> key should be structured like this:
453              
454             Module
455             Tag
456             Array of exceptions
457             "when"
458             Hash with conditions
459             "set"
460             Level to set if all conditions match
461              
462             The hash with conditions should have keys matching the attributes of the log entry that's being filtered (check the translation files to see what they are). The values for the keys should be either a single value that the attribute should be, or an array of values any one of which the attribute should be.
463              
464             A complete entry might could look like this:
465              
466             "SYSTEM": {
467             "FILTER_THIS": [
468             {
469             "when": {
470             "count": 1,
471             "type": ["this", "or"]
472             },
473             "set": "INFO"
474             },
475             {
476             "when": {
477             "count": 128,
478             "type": ["that"]
479             },
480             "set": "INFO"
481             },
482             {
483             "when": {
484             "count": 0
485             },
486             "set": "WARNING"
487             }
488             ]
489             }
490              
491             This would set the level to C<INFO> for any C<SYSTEM:FILTER_THIS> messages that had a C<count> attribute set to 1 and a C<type> attribute set to either C<this> or C<or>.
492             This also would set the level to C<INFO> for any C<SYSTEM:FILTER_THIS> messages that had a C<count> attribute set to 128 and a C<type> attribute set to C<that>.
493             And this would set the level to C<WARNING> for any C<SYSTEM:FILTER_THIS> messages that had a C<count> attribute set to 0.
494              
495             =head1 POLICY DATA
496              
497             Like the configuration data, policy data is stored in JSON format. Structurally, it's a bit less complex. All the keys on the top level, with one exception, are names of test implementation modules (without the C<Zonemaster::Engine::Test::> prefix). Each of those keys hold another hash, where the keys are the tags that the module in question can emit and the values are the the severity levels that should apply to the tags. Any tags that are not found in the policy data will default to level C<DEBUG>.
498              
499             The one exception is a top-level key C<__testcases__>. The value of that must be a hash where the keys are names of test cases from the test specifications, and the corresponding values are booleans specifying if the test case in question should be executed or not. Any missing test cases are treated as if they had the value C<true> set. The test cases C<basic00>, C<basic01> and C<basic02> will be executed even if their values are set to C<false>, since part of their function is to verify that the given name can be tested at all. The values here only apply when test modules are asked to run all their tests. A test case that is set to C<false> here will still run if asked for specifically.
500              
501             The easiest way to create a modified policy is to copy the default one and change the relevant values.
502              
503             =cut
504              
505             __DATA__
506             {
507             "asnroots" : [ "asnlookup.zonemaster.net", "asnlookup.iis.se"],
508             "net" : {
509             "ipv4" : 1,
510             "ipv6" : 1
511             },
512             "no_network" : 0,
513             "resolver" : {
514             "defaults" : {
515             "debug" : 0,
516             "dnssec" : 0,
517             "edns_size" : 0,
518             "igntc" : 0,
519             "recurse" : 0,
520             "retrans" : 3,
521             "retry" : 2,
522             "usevc" : 0
523             }
524             }
525             }