root/honeyclient/branches/bug/42/lib/HoneyClient/Util/Config.pm

Revision 96, 22.8 kB (checked in by kindlund, 2 years ago)

Completed registry parser documentation and unit tests; corrected minor mispellings; updated POD documentation to reflect public website.

  • Property svn:keywords set to Id "$file"
Line 
1 #######################################################################
2 # Created on:  Apr 20, 2006
3 # Package:     HoneyClient::Util::Config
4 # File:        Config.pm
5 # Description: Generic access to the HoneyClient configuration file.
6 #
7 # CVS: $Id$
8 #
9 # @author kindlund, flindiakos
10 #
11 # Copyright (C) 2006 The MITRE Corporation.  All rights reserved.
12 #
13 # This program is free software; you can redistribute it and/or
14 # modify it under the terms of the GNU General Public License
15 # as published by the Free Software Foundation, using version 2
16 # of the License.
17 #
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301, USA.
27 #
28 #######################################################################
29
30 =pod
31
32 =head1 NAME
33
34 HoneyClient::Util::Config - Perl extension to provide a generic interface
35 to the HoneyClient global configuration file.
36
37 =head1 VERSION
38
39 This documentation refers to HoneyClient::Util::Config version 1.0.
40
41 =head1 SYNOPSIS
42
43   use HoneyClient::Util::Config qw(getVar);
44
45   my $address = undef;
46  
47   # Fetch the value of "address" using the default namespace.
48   $address = getVar(name => "address");
49
50   # Fetch the value of "address" using the "HoneyClient::Agent::Driver" namespace.
51   $address = getVar(name      => "address",
52                     namespace => "HoneyClient::Agent::Driver");
53
54   # Fetch the value of "address" using the "HoneyClient::Manager" namespace.
55   $address = getVar(name      => "address",
56                     namespace => "HoneyClient::Manager");
57
58   # Set the value of "address" using the default namespace
59   setVar( name  => 'address',
60           value => 'new_address' );
61
62   # Set the value using a specified namespace
63   setVar( name      => 'address',
64           namespace => 'HoneyClient::Agent::Driver',
65           value     => 'new_address' );
66
67 =head1 DESCRIPTION
68
69 This library allows any HoneyClient module to quickly access the
70 global configuration options, associated with this program.
71
72 This library makes extensive use of the XML::XPath module.
73
74 =cut
75
76 package HoneyClient::Util::Config;
77
78 use strict;
79 use warnings;
80 use Carp ();
81 use XML::XPath;
82 use XML::Tidy;
83 use Log::Log4perl qw(:easy);
84 use Data::Dumper;
85
86 #######################################################################
87 # Module Initialization                                               #
88 #######################################################################
89
90 BEGIN {
91     # Defines which functions can be called externally.
92     require Exporter;
93     our (@ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS, $VERSION);
94
95     # Set our package version.
96     $VERSION = 0.9;
97
98     @ISA = qw(Exporter);
99
100     # Symbols to export on request
101     @EXPORT = qw(getVar setVar);
102
103     # Items to export into callers namespace by default. Note: do not export
104     # names by default without a very good reason. Use EXPORT_OK instead.
105     # Do not simply export all your public functions/methods/constants.
106
107     # This allows declaration use HoneyClient::Util::Config ':all';
108     # If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
109     # will save memory.
110
111     %EXPORT_TAGS = (
112         'all' => [ qw(getVar setVar) ],
113     );
114
115     # Symbols to autoexport (:DEFAULT tag)
116     @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
117
118     $SIG{PIPE} = 'IGNORE'; # Do not exit on broken pipes.
119 }
120 our (@EXPORT_OK, $VERSION);
121
122 =pod
123
124 =begin testing
125
126 # Make sure Log::Log4perl loads
127 BEGIN { use_ok('Log::Log4perl', qw(:nowarn))
128         or diag("Can't load Log::Log4perl package. Check to make sure the package library is correctly listed within the path.");
129        
130         # Suppress all logging messages, since we need clean output for unit testing.
131         Log::Log4perl->init({
132             "log4perl.rootLogger"                               => "DEBUG, Buffer",
133             "log4perl.appender.Buffer"                          => "Log::Log4perl::Appender::TestBuffer",
134             "log4perl.appender.Buffer.min_level"                => "fatal",
135             "log4perl.appender.Buffer.layout"                   => "Log::Log4perl::Layout::PatternLayout",
136             "log4perl.appender.Buffer.layout.ConversionPattern" => "%d{yyyy-MM-dd HH:mm:ss} %5p [%M] (%F:%L) - %m%n",
137         });
138 }
139 require_ok('Log::Log4perl');
140 use Log::Log4perl qw(:easy);
141
142 # Make sure the module loads properly, with the exportable
143 # functions shared.
144 BEGIN { use_ok('HoneyClient::Util::Config', qw(getVar setVar))
145         or diag("Can't load HoneyClient::Util::Config package.  Check to make sure the package library is correctly listed within the path."); }
146 require_ok('HoneyClient::Util::Config');
147 can_ok('HoneyClient::Util::Config', 'getVar');
148 can_ok('HoneyClient::Util::Config', 'setVar');
149 use HoneyClient::Util::Config qw(getVar setVar);
150
151 # Suppress all logging messages, since we need clean output for unit testing.
152 Log::Log4perl->init({
153     "log4perl.rootLogger"                               => "DEBUG, Buffer",
154     "log4perl.appender.Buffer"                          => "Log::Log4perl::Appender::TestBuffer",
155     "log4perl.appender.Buffer.min_level"                => "fatal",
156     "log4perl.appender.Buffer.layout"                   => "Log::Log4perl::Layout::PatternLayout",
157     "log4perl.appender.Buffer.layout.ConversionPattern" => "%d{yyyy-MM-dd HH:mm:ss} %5p [%M] (%F:%L) - %m%n",
158 });
159
160 # Make sure XML::XPath loads.
161 BEGIN { use_ok('XML::XPath')
162         or diag("Can't load XML::XPath package.  Check to make sure the package library is correctly listed within the path."); }
163 require_ok('XML::XPath');
164 can_ok('XML::XPath', 'findnodes');
165 use XML::XPath;
166
167 # Make sure XML::Tidy loads
168 BEGIN { use_ok('XML::Tidy')
169         or diag("Can't load XML::Tidy package. Check to make sure the package library is correctly listed within the path."); }
170 require_ok('XML::Tidy');
171 can_ok('XML::Tidy','tidy');
172 can_ok('XML::Tidy','write');
173 use XML::Tidy;
174
175 # Make sure Data::Dumper loads
176 BEGIN { use_ok('Data::Dumper')
177         or diag("Can't load Data::Dumper package. Check to make sure the package library is correctly listed within the path."); }
178 require_ok('Data::Dumper');
179 use Data::Dumper;
180
181 =end testing
182
183 =cut
184
185 #######################################################################
186
187 # Global Configuration Variables
188
189 # Relative path to the Global Configuration.
190 # Note: We leave this path relative, so that
191 # corresponding unit testing can work before
192 # we actually install the configuration
193 # file into /etc.
194 our $CONF_FILE = "etc/honeyclient.xml";
195
196 # The XPath object that points to the config file
197 our $xp;
198
199 # Temporarily Initialize Logging Subsystem
200 # Note: We use these sane values initially, until we can reinitialize
201 #       the logger with values from the global configuration file.
202 Log::Log4perl->init_once({
203     "log4perl.rootLogger"                               => "INFO, Screen",
204     "log4perl.appender.Screen"                          => "Log::Log4perl::Appender::ScreenColoredLevels",
205     "log4perl.appender.Screen.stderr"                   => 0,
206     "log4perl.appender.Screen.Threshold"                => "INFO",
207     "log4perl.appender.Screen.layout"                   => "Log::Log4perl::Layout::PatternLayout",
208     "log4perl.appender.Screen.layout.ConversionPattern" => "%d{yyyy-MM-dd HH:mm:ss} %5p [%M] (%F:%L) - %m%n",
209 });
210
211 # The global logging object.
212 our $LOG = get_logger();
213
214 # Make Dumper format more terse.
215 $Data::Dumper::Terse = 1;
216 $Data::Dumper::Indent = 0;
217
218 #######################################################################
219 # Private Methods Implemented                                         #
220 #######################################################################
221
222 # Helper function designed to read the global configuration file
223 #
224 # Inputs: config
225 # Outputs: None
226 sub _parseConfig {
227
228     # Extract arguments.
229     my ($class, $config) = @_;
230
231     # Sanity check.  Make sure the file exists.
232     if (!-f $config) {
233         # Okay, if the relative path didn't work, try the absolute
234         # path.
235         $config = "/" . $config;
236         if (!-f $config) {
237             $LOG->fatal("Unable to parse global configuration file ($CONF_FILE)!");
238             Carp::croak("Error: Unable to parse global configuration file ($CONF_FILE)!");
239         }
240         # The absolute path worked, update the global variable to reflect this.
241         $CONF_FILE = $config;
242     }
243
244     # Read in the configuration settings.
245     eval {
246         $xp = XML::XPath->new(filename => $CONF_FILE);
247     };
248
249     # Sanity check
250     if ($@ || !$xp->exists("HoneyClient")) {
251         $LOG->fatal("Unable to parse global configuration file ($CONF_FILE)!" . $@);
252         Carp::croak("Error: Unable to parse global configuration file ($CONF_FILE)!" . $@);
253     }
254 }
255
256 # Helper function designed to check the arguments passed to getVar()
257 #
258 # Inputs: $args
259 # Outputs: None
260 sub _checkArgs{
261     # Hashref of arguments
262     our ($args) = @_;
263
264     # Make sure we have args
265     if (!%$args) {
266         $LOG->fatal("No variables specified!");
267         Carp::croak("Error: No variables specified!");
268     }
269
270     # Process the args
271     #   If you do not specify a default value, it will croak if undefined
272     _process('name');
273     _process('namespace', caller(1)); # We want the namespace of the caller to getVar(),
274                                       # not of the caller to _checkArgs(); hence, we
275                                       # use caller(1).
276
277     # Add any special statements to check for depending on the caller
278     # Just specify the calling sub in the regex and any operations in the do{}
279     #   Why can't perl actually have switch statements :(
280     for((split(/::/,((caller(1))[3])))[-1]){
281         /getVar/    && do {  };
282         /setVar/    && do { _process('value') };
283     }
284
285
286     # Accepts the key to check and the default value.
287     # If no default value is given, undef will be used
288     sub _process{
289         my ($name, $val) = @_;
290         if ( !defined($args->{$name} )) {
291             $args->{$name} = $val;
292
293             # Sanity checking after
294             unless( $args->{$name} ) {
295                 $LOG->fatal("No variable $name specified!");
296                 Carp::croak("Error: No variable $name specified!");
297             }
298         }
299     }
300 }
301
302 #######################################################################
303 # Public Methods Implemented                                          #
304 #######################################################################
305
306 =pod
307
308 =head1 EXPORTS
309
310 =head2 getVar(name => $varName, namespace => $caller, attribute => $attribute)
311
312 =over 4
313
314 If $attribute is undefined or not specified, then this function will
315 attempt to retrieve the contents of the B<element> $varName, as it is set
316 within the HoneyClient global configuration file.
317
318 If $attribute is defined, then this function will attempt to retrieve
319 specified B<attribute> listed within the contents the contents of the
320 element $varName, as it is set within the HoneyClient global configuration
321 file.
322
323 If $caller is undefined or not specified, then this function may return
324 different values, depending upon which module is calling this function.
325
326 For example, if module HoneyClient::Agent::Driver calls this function
327 as getVar(name => "address"), then this function will attempt to search for
328 a value like the following, within the global configuration file:
329
330 <HoneyClient>
331     <Agent>
332         <Driver>
333             <address>localhost</address>
334         </Driver>
335     </Agent>
336 </HoneyClient>
337
338 If the "address" value is not found at this level within the XML tree,
339 then the function will attempt to locate values, like the following:
340
341 # First try:
342
343 <HoneyClient>
344     <Agent>
345         <address>localhost</address>
346     </Agent>
347 </HoneyClient>
348
349 # Last try:
350
351 <HoneyClient>
352     <address>localhost</address>
353 </HoneyClient>
354
355 This function will stop its recursive search at the first value found,
356 closest to the child module's XML namespace.
357
358 Even after performing a recursive search, if no variable name exists,
359 then the function will croak with errors.
360
361 I<Inputs>:
362  B<$varName> is the variable name to search for, within the global
363 configuration file.
364  B<$caller> is an optional argument, signifying the module namespace
365 to use, when searching for the variable's value.
366  B<$attribute> is an optional argument, signifying that the function
367 should return the attribute associated with the variable's element.
368
369 I<Output>: The variable's element/attribute value, if found; warns and
370 returns undef otherwise.
371
372 =back
373
374 =begin testing
375
376 my $value = getVar(name => "address", namespace => "HoneyClient::Util::Config::Test");
377 is($value, "localhost", "getVar(name => 'address', namespace => 'HoneyClient::Util::Config::Test')")
378     or diag("The getVar() call failed.  Attempted to get variable 'address' using namespace 'HoneyClient::Util::Config::Test' within the global configuration file.");
379
380 $value = getVar(name => "address", namespace => "HoneyClient::Util::Config::Test", attribute => 'default');
381 is($value, "localhost", "getVar(name => 'address', namespace => 'HoneyClient::Util::Config::Test', attribute => 'default')")
382     or diag("The getVar() call failed.  Attempted to get attribute 'default' for variable 'address' using namespace 'HoneyClient::Util::Config::Test' within the global configuration file.");
383
384 # This check tests to make sure getVar() is able to use valid output
385 # from undefined namespaces (but where some of the parent namespace is
386 # partially known).
387 $value = getVar(name => "address", namespace => "HoneyClient::Util::Config::Test::Undefined::Child", attribute => 'default');
388 is($value, "localhost", "getVar(name => 'address', namespace => 'HoneyClient::Util::Config::Test::Undefined::Child', attribute => 'default')")
389     or diag("The getVar() call failed.  Attempted to get attribute 'default' for variable 'address' using namespace 'HoneyClient::Util::Config::Test::Undefined::Child' within the global configuration file.");
390
391 =end testing
392
393 =cut
394
395 sub getVar {
396
397     # Get the arguments and check their validity
398     my (%args) = @_;
399     _checkArgs(\%args);
400
401     # Log resolved arguments.
402     # Make Dumper format more terse.
403     $Data::Dumper::Terse = 1;
404     $Data::Dumper::Indent = 0;
405     $LOG->debug(Dumper(\%args));
406    
407     # Get a copy of the original namespace.
408     my $namespace = $args{namespace};
409
410     # Fix the namespace so it is compatible with XPath
411     $namespace =~ s/::/\//g; # Turn package delim :: into XPath delim /
412
413     # Split the namespace into an array.
414     my @ns = split(/\//, $namespace);
415
416     # Check to make sure the namespace exists within our XML configuration.
417     # XML::XPath does not know how to deal with unknown paths (even if the parent
418     # path is known).  Thus, we recursively check the path's existance, providing
419     # the first valid ancestor path found.
420     while (!$xp->exists($namespace) and
421            (scalar(@ns) > 1)) {
422         pop(@ns);
423         $namespace = join('/', @ns);
424         @ns = split(/\//, $namespace);
425     }
426
427     # Get the nodeset that we need
428     # The first string is the path that matches the node we want and all ancestors
429     # The second string tells us whether to get the text() or an attribute
430     my $exp = $namespace . "/ancestor-or-self::*/$args{name}/" .
431         (defined $args{attribute} ? "attribute::" . $args{attribute}:"text()");
432     my $nodeset = $xp->findnodes($exp);
433
434     # The list of nodes required.  Because this is a top down list of the results,
435     # if there are multiple results, we want the bottom one (most specific)
436     if ($nodeset->size() == 0) {
437         $LOG->warn("Warning: Unable to locate specified value in variable '" .
438                    $args{'name'} . "' using namespace '" . $args{'namespace'} .
439                    "' within the global configuration file ($CONF_FILE)!");
440         return;
441     }
442     my $val = $nodeset->pop->string_value;
443
444     # Trail leading and trailing whitespace
445     $val =~ s/^\s+|\s+$//g;
446
447     # For some reason attributes return attribute_name="value"
448     $val =~ s/.*"(.*)".*/$1/;
449
450     return $val;
451 }
452
453 =pod
454
455 =head1 EXPORTS
456
457 =head2 setVar(name => $varName, namespace => $caller, attribute => $attribute, value => $value)
458
459 =over 4
460
461 This will set the desired value.
462 If the required attribute or element does not exist, it (and any parents) will be created
463
464 I<Inputs>:
465  B<$varName> is the variable name to search for, within the global
466 configuration file.
467  B<$caller> is an optional argument, signifying the module namespace
468 to use, when searching for the variable's value.
469  B<$attribute> is an optional argument, signifying that the function
470 should return the attribute associated with the variable's element.
471  B<$value> is the value to set the element or attribute to
472
473 =back
474
475 =begin testing
476
477 # Test setting an existing value
478 my $oldval = getVar(name => 'address', namespace => 'HoneyClient::Util::Config::Test' );
479 setVar(name => 'address', namespace => 'HoneyClient::Util::Config::Test', value => 'foobar' );
480 my $value = getVar(name => 'address', namespace => 'HoneyClient::Util::Config::Test' );
481 is($value, 'foobar', "setVar(name => 'address', namespace => 'HoneyClient::Util::Config::Test', value => 'foobar' )")
482     or diag("The setVar() call failed.  Attempted to set variable 'address' using namespace 'HoneyClient::Util::Config::Test' to 'foobar' within the global configuration file.");
483 setVar(name => 'address', namespace => 'HoneyClient::Util::Config::Test', value => $oldval );
484
485 # Test setting an attribute
486 $oldval = getVar(name => 'address', attribute => 'default', namespace => 'HoneyClient::Util::Config::Test' );
487 setVar(name => 'address', namespace => 'HoneyClient::Util::Config::Test', attribute => 'default', value => 'foobar' );
488 $value = getVar(name => 'address', attribute => 'default', namespace => 'HoneyClient::Util::Config::Test' );
489 is($value, 'foobar', "setVar(name => 'address', namespace => 'HoneyClient::Util::Config::Test', attribute => 'default', value => 'foobar' )")
490     or diag("The setVar() call failed.  Attempted to set 'default' attribute of variable 'address' using namespace 'HoneyClient::Util::Config::Test' to 'foobar' within the global configuration file.");
491 setVar(name => 'address', namespace => 'HoneyClient::Util::Config::Test', attribute => 'default', value => $oldval );
492
493 # Test creating a value
494 setVar(name => 'zingers', namespace => 'HoneyClient::Util::Config::Test', value => 'foobar');
495 $value = getVar(name => 'zingers', namespace => 'HoneyClient::Util::Config::Test' );
496 is($value, 'foobar', "setVar(name => 'zingers', namespace => 'HoneyClient::Util::Config::Test', value => 'foobar' )")
497     or diag("The setVar() call failed.  Attempted to create variable 'zing' using namespace 'HoneyClient::Util::Config::Test' with a value of 'foobar' within the global configuration file.");
498
499 # Test creating an attribute
500 setVar(name => 'address', namespace => 'HoneyClient::Util::Config::Test', attribute => 'zing', value => 'foobar');
501 $value = getVar(name => 'address', attribute => 'zing', namespace => 'HoneyClient::Util::Config::Test' );
502 is($value, 'foobar', "setVar(name => 'address', namespace => 'HoneyClient::Util::Config::Test', attribute => 'zing', value => 'foobar' )")
503     or diag("The setVar() call failed.  Attempted to create attribute 'zing' using namespace 'HoneyClient::Util::Config::Test' with a value of 'foobar' within the global configuration file.");
504
505 # Creating new namespaces
506 setVar(name => 'address', namespace => 'HoneyClient::Util::Config::Test::Foo::Bar', value => 'baz');
507 $value =  getVar(name => 'address', namespace => 'HoneyClient::Util::Config::Test::Foo::Bar');
508 is($value, 'baz', "setVar(name => 'address', namespace => 'HoneyClient::Util::Config::Test::Foo::Bar', value => 'baz')")
509     or diag("The setVar() call failed.  Attempted to create attribute 'address' using namespace 'HoneyClient::Util::Config::Test::Foo::Bar' with a value of 'baz' within global configuration file.");
510
511 =end testing
512
513 =cut
514
515 sub setVar {
516     # Get the arguments and check their validity
517     my (%args) = @_;
518     _checkArgs(\%args);
519
520     # Log resolved arguments.
521     # Make Dumper format more terse.
522     $Data::Dumper::Terse = 1;
523     $Data::Dumper::Indent = 0;
524     $LOG->debug(Dumper(\%args));
525
526     # Fix the namespace so it is compatible with XPath
527     my $namespace = $args{namespace};
528     $namespace =~ s/::/\//g; # Turn package delim :: into XPath delim /
529
530     # Get the nodeset that we need
531     # The first string is the path that matches the node we want
532     # The second string tells us whether to get the text() or an attribute
533     my $exp = $namespace . "/$args{name}" .
534         (defined $args{attribute} ? "/attribute::" . $args{attribute} : "");
535     if(!$xp->exists($exp)){
536         $xp->createNode($exp);
537     }
538     $xp->setNodeText($exp,$args{value});
539
540     # Create the tidy object with our document root and write out the stuff to the new conf_file
541     my $tidy_obj = XML::Tidy->new(context => $xp->find('/'));
542     $tidy_obj->tidy('    ');
543     $tidy_obj->write($CONF_FILE);
544
545     # Parse the conf_file again just for good measure
546     _parseConfig(undef, $CONF_FILE);
547 }
548
549 #######################################################################
550
551 # Parse the global configuration file, upon using the package.
552 _parseConfig(undef, $CONF_FILE);
553
554 # Reinitialize Logging Subsystem
555 # TODO: Need to account for absolute "/etc" directories!
556 Log::Log4perl->init(getVar(name => "log_config"));
557
558 1;
559
560 #######################################################################
561 # Additional Module Documentation                                     #
562 #######################################################################
563
564 __END__
565
566 =head1 BUGS & ASSUMPTIONS
567
568 This module assumes the HoneyClient global configuration file is located
569 in: /etc/honeyclient.conf
570
571 The getVar($varName) function will attempt to get a module-specific
572 variable setting, first.  If that setting is not specified, the function
573 call will recursively search for the same variable located within any
574 parent (or global) regions of the configuration file.  See the getVar()
575 documentation for further details.
576
577 =head1 SEE ALSO
578
579 L<http://www.honeyclient.org/trac>
580
581 XML::XPath
582
583 =head1 REPORTING BUGS
584
585 L<http://www.honeyclient.org/trac/newticket>
586
587 =head1 AUTHORS
588
589 Darien Kindlund, E<lt>kindlund@mitre.orgE<gt>
590
591 Fotios Lindiakos, E<lt>flindiakos@mitre.orgE<gt>
592
593 =head1 COPYRIGHT & LICENSE
594
595 Copyright (C) 2006 The MITRE Corporation.  All rights reserved.
596
597 This program is free software; you can redistribute it and/or
598 modify it under the terms of the GNU General Public License
599 as published by the Free Software Foundation, using version 2
600 of the License.
601
602 This program is distributed in the hope that it will be useful,
603 but WITHOUT ANY WARRANTY; without even the implied warranty of
604 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
605 GNU General Public License for more details.
606
607 You should have received a copy of the GNU General Public License
608 along with this program; if not, write to the Free Software
609 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
610 02110-1301, USA.
611
612
613 =cut
614
615 <!--
616     vim: foldmarker==pod,=cut
617 -->
Note: See TracBrowser for help on using the browser.