1#! /usr/bin/perl
2#
3#
4# $Id: DBStore.pm 109 2009-10-17 22:00:16Z lem $
5
6package Net::Radius::Server::DBStore;
7
8use 5.010;
9use strict;
10use warnings;
11
12use Storable qw/freeze/;
13
14use Net::Radius::Server::Base qw/:set/;
15use base 'Net::Radius::Server::Base';
16__PACKAGE__->mk_accessors(qw/key_attrs param store result sync
17			  pre_store_hook single frozen hashref
18			  internal_tie/);
19our $VERSION = do { sprintf "%0.3f", 1+(q$Revision: 109 $ =~ /\d+/g)[0]/1000 };
20
21sub mk
22{
23    my $class = shift;
24    die "->mk() cannot have arguments when in object-method mode\n"
25	if ref($class) and $class->isa('UNIVERSAL') and @_;
26
27    my $self = $class;
28
29    if (@_)
30    {
31	$self = $class->new(@_);
32	die "Failed to create new object\n" unless $self;
33    }
34
35    die "->mk() cannot proceed with no valid param defined\n"
36	unless ref($self->param) eq 'ARRAY';
37
38    # Enforce default values
39
40    $self->frozen(1)       unless defined $self->frozen();
41    $self->single(1)       unless defined $self->single();
42    $self->internal_tie(1) unless defined $self->internal_tie();
43
44    $self->key_attrs([ 'NAS-IP-Address', '|', 'Acct-Session-Id' ])
45	unless $self->key_attrs();
46
47    $self->store([ qw/ packet peer_addr peer_host peer_port port /])
48	unless $self->store();
49
50    $self->sync(1)
51	unless defined $self->sync();
52
53    $self->log_level(1)
54	unless defined $self->log_level();
55
56    # Create the tied hash that we will be passing into the actual method.
57
58    my ($db, %hash);
59    my ($c, @params) = (@{$self->param});
60
61    $self->hashref(\%hash) unless $self->hashref;
62
63    $self->log(2, "Tying to class '" . $c  . "'");
64    $self->log(3, "Tie parameters are " . join(', ', @params));
65    if ($self->internal_tie)
66    {
67	eval { $db = tie %{$self->hashref}, $c, @params };
68
69	die "->mk() unable to tie: $!" unless $db;
70	die "->mk() problem during tie: $@" if $@;
71    }
72    else
73    {
74	$self->log(2, "Not tying because ->internal_tie is true");
75    }
76
77    return sub { $self->_do_tie( $db, $self->hashref, @_ ) };
78}
79
80# Convert a scalar into the corresponding Radius attribute in
81# $req. Will return non-matched scalars, to be used as delimiters in
82# the resulting key.
83sub _k
84{
85    my ($self, $db, $rhash, $r_data, $req, $attr) = @_;
86    my $v = undef;
87
88    if (ref($attr) eq 'ARRAY')
89    {
90	$v = $req->vsattr(@$attr);
91	return $v->[0] if ref($v) eq 'ARRAY';
92	return $v if defined $v;
93	return '';
94    }
95    elsif (ref($attr) eq 'CODE')
96    {
97	return $attr->($self, $db, $rhash, $r_data, $req);
98    }
99    else
100    {
101	$v = $req->attr($attr);
102	return $v if defined $v;
103    }
104
105    return $attr;
106}
107
108sub _do_tie
109{
110    my $self   = shift;
111    my $db     = shift;
112    my $rhash  = shift;
113    my $r_data = shift;
114
115    my $req    = $r_data->{request};
116
117    $self->log(2, 'Storing data');
118    $self->log(4, "self=$self rhash=$rhash r_data=$r_data");
119
120    # Find the key to store
121    my $key = join('', (map { $self->_k($db, $rhash, $r_data, $req, $_) }
122			@{$self->key_attrs}));
123    $self->log(4, 'Storing data using key "' . $key . '"');
124
125    # Invoke hook, if available
126    my $f = undef;
127    if ($f = $self->pre_store_hook()
128	and ref($f) eq 'CODE')
129    {
130	$self->log(3, 'Invoking pre_store_hook');
131	# Note that the pre_store_hook could change object's config...
132	$f->($self, $db, $rhash, $r_data, $req, $key);
133    }
134    else
135    {
136	$self->log(4, 'no pre_store_hook');
137    }
138
139    # Find what to store
140    my @store = @{$self->store};
141    $self->log(4, 'Storing the following items: ' . join(', ', @store));
142    my %data = map { $_ => $r_data->{$_} } @store;
143
144    if ($self->single)
145    {
146	$self->log(4, "Single Store $key: ", \%data);
147	$rhash->{$key} = ($self->frozen ? freeze \%data : \%data);
148    }
149    else
150    {
151	while (my ($k, $v) = each %data)
152	{
153	    $self->log(4, "Non-Single Store $key->$k: $v");
154	    $rhash->{$key}->{$k} = ($self->frozen ? freeze $v : $v)
155	}
156    }
157    $self->log(4, "tuple contains: " . $rhash->{$key} // 'undef');
158
159    # Force sync writes
160    $db->db_sync if $db and $self->sync and $db->can('db_sync');
161
162    if ($self->can('result') and exists $self->{result})
163    {
164	my $r = $self->result;
165	$self->log(3, "Returning $r");
166	return $r;
167    }
168
169    $self->log(3, "Returning CONTINUE by default");
170    return NRS_SET_CONTINUE;
171}
172
17342;
174
175__END__
176
177=head1 NAME
178
179Net::Radius::Server::DBStore - Store Radius packets into a Tied Hash
180
181=head1 SYNOPSIS
182
183  use MLDBM::Sync;
184  use MLDBM qw(DB_File Storable);
185  use Net::Radius::Server::DBStore;
186  use Net::Radius::Server::Base qw/:set/;
187
188  my $obj = Net::Radius::Server::DBStore->new
189    ({
190      log_level      => 4,
191      param          => [ 'MLDBM::Sync',
192                          @Tie_Opts ],
193      store          => [qw/packet peer_addr port/],
194      pre_store_hook => sub { ... },
195      sync           => 1,
196      single         => 1,
197      internal_tie   => 1,
198      frozen         => 0,
199      key_attrs      => [ 'Acct-Session-Id', [ Vendor => 'PrivateSession' ] ],
200      hashref        => \%external_hash,
201      result         => NRS_SET_CONTINUE,
202    });
203
204  my $sub = $obj->mk();
205
206  # or
207
208  my $sub = Net::Radius::Server::DBStore->mk
209    ({
210    # ... same parameters as above ...
211    });
212
213=head1 DESCRIPTION
214
215C<Net::Radius::Server::DBStore> is a match or set method factory than
216can be used within C<Net::Radius::Server::Rule> objects.
217
218Note that this factory can produce either match or set methods. The
219only practical difference is the actual result to be returned, that
220defaults to C<NRS_SET_CONTINUE>. This is so, as it is anticipated that
221the most common use for this class would be producing set methods, so
222that accounting packets can be stored after classification that can be
223made using corresponding match methods.
224
225You can trivially replace the result to be returning by using the
226C<result> key, as shown in the SYNOPSIS.
227
228=over
229
230=item C<-E<gt>new($hashref)>
231
232Creates a new Net::Radius::Server::DBStore(3) object that acts as aod
233factory. C<$hashref> referenes a hash with the attributes that will
234apply to this object, so that multiple methods (that will share the
235same underlying object) can be created and given to different rules.
236
237=item C<-E<gt>mk($hashref)>
238
239Invokes C<-E<gt>new()> passing the given C<$hashref> if needed.
240
241At this stage, an object-private hash is tied to the specified class
242(MLDBM::Sync(3) as in the SYNOPSIS), using the given flags. This
243hash is stored in the object and will be shared by any methods
244constructed from it.
245
246This makes more efficient the case where you want to store information
247coming from various different rules, such as when matching for
248different types of service, more efficient.
249
250C<-E<gt>mk()> then returns a method that is suitable to be employed as
251either a match or set method within a C<Net::Radius::Server::Rule>
252object.
253
254=item C<$self-E<gt>mk()> or C<__PACKAGE__-E<gt>mk($hashref)>
255
256This method returns a sub suitable for calling as either a match or
257set method for a C<Net::Radius::Server::Rule> object. The resulting
258sub will return C<NRS_SET_CONTINUE> by default, unless overriden by
259the given configuration.
260
261The sub contains a closure where the object attributes -- Actually,
262the object itself -- are kept.
263
264When invoked as an object method (ie, C<$self-E<gt>mk()>), no
265arguments can be given. The object is preserved as is within the
266closure.
267
268When invoked as a class method (ie, C<__PACKAGE__-E<gt>mk($hashref)>),
269a new object is created with the given arguments and then, this object
270is preserved within the closure. This form is useful for compact
271filter definitions that require little or no surrounding code or
272holding variables.
273
274=item C<-E<gt>_do_tie()>
275
276You're not supposed to call this method directly. It is called by the
277sub produced with C<-E<gt>mk()>. Within this method, the following
278takes place:
279
280=over
281
282=item *
283
284The record key is calculated by using the corresponding configuration
285entry.
286
287=item *
288
289The requested information is stored in the tied hash, thus inserted in
290the underlying storage method.
291
292=item *
293
294The required return value is passed back to the caller.
295
296=back
297
298=back
299
300=head2 Configuration Keys
301
302The following configuration keys are understood by this class, in
303addition to the ones handled by Net::Radius::Server::Base(3). Note
304that those are available in the factory object (the one retured by the
305call to C<-E<gt>new()>) as same-name accessors.
306
307=over
308
309=item B<param     =E<gt> [ @args ]>
310
311The actual parameters to the C<tie>. This parameter is mandatory. The
312first item in the C<@args> list has to be the name of the class to
313tie. Tipically you will want to use MLDBM(3), MLDBM::Sync(3),
314BerkeleyDB::Hash(3) or Tie::DBI(3).
315
316    param => [ 'MLDBM::Sync', '/my/db/file.db' ],
317
318Note that concurrency will be an issue. You need to insure that you
319use modules and settings that consider the fact that multiple
320instances will be writing at the same time.
321
322=item B<key_attrs =E<gt> [ @keys ]>
323
324Specify the Radius attributes to use as the record key for accessing
325the database. Each element of the list can be one of the following types:
326
327=over
328
329=item B<Scalar>
330
331This is either an attribute name or a delimiter. Actually, any string
332is used to look up the corresponding attribute in the request
333packet. If this fails, the actual string is inserted as the value of
334the key. Upon success, the value of the corresponding attribute is
335inserted in the key.
336
337=item B<sub or CODE ref>
338
339This sub will be called with the following arguments: The
340Net::Radius::Server::DBStore(3) object, the C<tied()> object as
341returned by C<tie>, a reference to the tied hash, a reference to the
342hash with data passed to the method and a Net::Radius::Packet(3)
343object with the decoded request this rule is responding to.
344
345The return value of the sub will be inserted in the key.
346
347This is useful to create hash keys that depend on information not
348within the actual Radius request.
349
350=item B<ArrayRef>
351
352This is interpreted as a VSA. The first element of the given list
353encodes the vendor name. The second attribute encodes the vendor
354attribute name.
355
356If the attribute is found within the request packet, its value is
357substituted at the current location of the key. Otherwise, an empty
358string will be substituted in its place.
359
360=back
361
362The following example:
363
364      key_attrs => [ 'Acct-Session-Id', '|', [ Cisco => 'Foo' ] ]
365
366Would produce a key like this:
367
368      DEADBEEF872374628742|
369
370Or if the ficticious VSA was defined, something like
371
372      DEADBEEF872374628742|The_Value
373
374The default attribute list is C<[ 'NAS-IP-Address', '|',
375'Acct-Session-Id' ]> which is likely to be suitable for Radius
376accounting packets. Note that RFC-2866 states that the
377C<Acct-Session-Id> attribute is unique, but this is generally so
378within a single device. When multiple devices are served, there may be
379a chance of collision. Including the IP Address of the NAS helps solve
380the problem. You must review your own environment and insure that the
381given key will produce unique values for each session.
382
383=item B<store =E<gt> [ @items ]>
384
385Tells the method which pieces of information to store within the tied
386hash. This corresponds to the attributes that are passed to the actual
387method. You might want to take a look at Net::Radius::Server::NS(3)
388and Net::Radius::Server::Rule(3) for more information.
389
390You should be conservative with this config entry, to store only as
391much information as needed. Note that you might be storing potentially
392sensitive information, such as user passwords, so appropiate care
393should be taken.
394
395The dafault value for C<@items> is C<packet, peer_addr, peer_host,
396peer_port, port>. This default should avoid storing huge objects
397alongside the useful data.
398
399Be aware that storing decoded packets (ie, including either C<request>
400or C<response> on the list of C<@items>) will lead to storing the NAS
401shared secret and the dictionaries using to encode and decode the
402packets. This will be large.
403
404=item B<pre_store_hook =E<gt> $sub>
405
406This C<$sub> will be called before actually calculating and storing in
407the BerkeleyDB(3) database. The following arguments are passed, in
408this order: The Net::Radius::Server::DBStore(3) object, the
409C<tied()> object as returned, a reference to the tied
410hash, a reference to the hash with data passed to the method, a
411Net::Radius::Packet(3) object with the decoded request this rule is
412responding to and the calculated key for this entry.
413
414The return value of the sub is currently ignored.
415
416=item B<sync =E<gt> $value>
417
418Causes a call to C<-E<gt>db_sync()> after each insertion when
419C<$value> evaluates to true, which is the default. When set to a false
420value, no calls will be made.
421
422The call to C<-E<gt>db_sync()> probably causes a performance hit.
423
424=item B<single =E<gt> $value>
425
426When set to true (the default), stores all the required elements as a
427single hash. When set to false, each tuple is stored individually
428within a hashref associated to the key.
429
430=item B<frozen =E<gt> $value>
431
432When set to true (the default), uses C<freeze()> from Storable(3) to
433serialize the values prior to storing.
434
435=item B<internal_tie =E<gt> $value>
436
437When true, the default, C<tie()> will be performed on the hash. In
438certain cases, you might want to "share" a hash. In these cases, the
439actual tying can be done elsewhere.
440
441=item B<hashref =E<gt> $hashref>
442
443Tells the factory to work with an external hash. This is useful to
444have external code modifying the underlying hash outside of a RADIUS
445transaction.
446
447If not provided, each call to C<-E<gt>mk()> ties a private hash. Note
448that you can use C<hashref> in a call to C<-E<gt>new()>, and then all
449the functions generated with C<-E<gt>mk()> will share the same hash.
450
451=back
452
453=head2 EXPORT
454
455None by default.
456
457=head1 BUGS
458
459This code uses C<die()> currently, however it is likely that
460C<croak()> would be better. The problem with this, is that using
461C<croak()> as intended, results in Perl returning errors like this
462one...
463
464    Bizarre copy of HASH in sassign
465        at /usr/share/perl/5.10/Carp/Heavy.pm line 96.
466
467while running C<make test> in my test machine. Since I don't want to
468run any risks, I'll stick to the C<die()> calls which do not
469manipulate the stack so much.
470
471=head1 SEE ALSO
472
473Perl(1), BerkeleyDB(3), Class::Accessor(3), MLDBM(3), MLDBM::Sync(3),
474Net::Radius::Packet(3), Net::Radius::Server(3),
475Net::Radius::Server::Base(3), Net::Radius::Server::NS(3),
476Net::Radius::Server::Rule(3), Storable(3), Tie::DBI(3).
477
478=head1 AUTHOR
479
480Luis E. Muñoz, E<lt>luismunoz@cpan.orgE<gt>
481
482=head1 COPYRIGHT AND LICENSE
483
484Copyright (C) 2006-2009 by Luis E. Muñoz
485
486This library is free software; you can redistribute it and/or modify
487it under the terms of the GPL version 2.
488
489=cut
490
491
492