File Coverage

blib/lib/WWW/Authen/Simple.pm
Criterion Covered Total %
statement 21 204 10.2
branch 0 130 0.0
condition 0 36 0.0
subroutine 7 28 25.0
pod 14 15 93.3
total 42 413 10.1


line stmt bran cond sub pod time code
1             package WWW::Authen::Simple;
2              
3             # $Source: /usr/local/cvs/WWW-Authen-Simple/pm/Simple.pm,v $
4             # $Revision: 1.24 $
5             # $Date: 2004/05/12 03:21:32 $
6             # $Author: jmiller $
7              
8 1     1   937 use 5.00503;
  1         3  
  1         38  
9 1     1   5 use strict;
  1         2  
  1         38  
10 1     1   15 use Digest::MD5 ();
  1         2  
  1         19  
11 1     1   5 use Carp;
  1         1  
  1         62  
12 1     1   1864 use CGI qw(:standard);
  1         13552  
  1         14  
13 1     1   4061 use lib './';
  1         606  
  1         4  
14              
15 1     1   88 use vars qw($VERSION $REVISION);
  1         3  
  1         2889  
16              
17             $REVISION = sprintf "%d.%03d", q$Revision: 1.24 $ =~ /(\d+)/g;
18             $VERSION = '1.22';
19              
20             # Config for table layout and such.
21             # we'll provide methods to override this, so that WWW:A:S can
22             # adapt to other systems
23             my $conf = {
24             session_table => {
25             _table => 'sessions', # table name
26             login => 'username', # username field
27             address => 'address', # remote address field
28             ticket => 'ticket', # session ticket field
29             point => 'point', # timestamp point field
30             },
31             user_table => {
32             _table => 'Users', # table name
33             uid => 'uid', # user unique id field
34             login => 'login', # username field
35             passwd => 'passwd', # password field
36             status => 'disabled', # status field
37             # sub ref to determine if status value is active
38             _active_status => sub { return 1 if ($_[0] != 1); },
39             # sub ref to determine if status value is disabled
40             _disabled_status => sub { return 1 if ($_[0] == 1); },
41             },
42             # group statement is used to get the groups. It should
43             # fetch a groupname, groupid, and an accessbit.
44             # If you don't want to use the accessbit field, just stick
45             # the groupid field there as well.
46             # %uid% will be replaced with a quoted uid value for the user.
47             # here's an alternate statement, to give you some ideas:
48             # SELECT groupname, gid, gid FROM Users WHERE uid = %uid%
49             group_statement => 'SELECT g.Name, ug.gid, ug.accessbit
50             FROM Groups g, UserGroups ug
51             WHERE g.gid = ug.gid AND ug.uid = %uid%',
52             # subroutine ref used to encrypt password for db storage
53             'crypt' => sub { return Digest::MD5::md5_base64($_[0]); }
54             };
55              
56             sub new
57             {
58 0     0 1   my ($this) = shift;
59 0   0       my $class = ref($this) || $this;
60 0           my $self = {};
61 0           bless( $self, $class );
62            
63 0           my %opts = @_;
64            
65 0           $self->conf($conf);
66 0 0 0       my $debug = (defined($opts{debug}) && ($opts{debug} =~ /^\d+$/))
67             ? $opts{debug} : 0;
68 0           $self->debug($debug);
69              
70 0 0 0       my $expire_seconds = (defined($opts{expire_seconds}) && ($opts{expire_seconds} =~ /^\d+$/))
71             ? $opts{expire_seconds} : 3600;
72 0           $self->expire_seconds($expire_seconds);
73              
74 0 0 0       my $cleanup_seconds = (defined($opts{cleanup_seconds}) && ($opts{cleanup_seconds} =~ /^\d+$/))
75             ? $opts{cleanup_seconds} : 43200;
76 0           $self->cleanup_seconds($cleanup_seconds);
77              
78 0           $self->cookie_domain($opts{cookie_domain});
79 0 0         $self->db($opts{db}) if($opts{db});
80              
81 0           return $self;
82             }
83              
84             sub db
85             {
86 0 0   0 1   ref(my $self = shift) or croak "instance variable needed";
87 0 0         if (@_)
88             {
89 0           $self->{_db} = $_[0];
90 0           return $self->{_db};
91             } else {
92 0           return $self->{_db};
93             }
94             }
95              
96             sub cookie_domain
97             {
98 0 0   0 1   ref(my $self = shift) or croak "instance variable needed";
99 0 0         if (@_)
100             {
101 0           $self->{_cookie_domain} = $_[0];
102 0           return $self->{_cookie_domain};
103             } else {
104 0           return $self->{_cookie_domain};
105             }
106             }
107              
108             sub expire_seconds
109             {
110 0 0   0 0   ref(my $self = shift) or croak "instance variable needed";
111 0 0         if (@_)
112             {
113 0 0         croak "expire must be a possitive integer" unless ($_[0] =~ /^\d+$/);
114 0           $self->{_expire_seconds} = $_[0];
115 0           return $self->{_expire_seconds};
116             } else {
117 0           return $self->{_expire_seconds};
118             }
119             }
120              
121             sub cleanup_seconds
122             {
123 0 0   0 1   ref(my $self = shift) or croak "instance variable needed";
124 0 0         if (@_)
125             {
126 0 0         croak "expire must be a possitive integer" unless ($_[0] =~ /^\d+$/);
127 0           $self->{_cleanup_seconds} = $_[0];
128 0           return $self->{_cleanup_seconds};
129             } else {
130 0           return $self->{_cleanup_seconds};
131             }
132             }
133              
134             sub debug
135             {
136 0 0   0 1   ref(my $self = shift) or croak "instance variable needed";
137 0 0         if (@_)
138             {
139 0           $self->{_debug} = $_[0];
140 0           return $self->{_debug};
141             } else {
142 0           return $self->{_debug};
143             }
144             }
145              
146              
147             sub cleanup
148             {
149 0 0   0 1   ref(my $self = shift) or croak "instance variable needed";
150              
151 0           my $cleanup_point = time() - $self->cleanup_seconds();
152 0           $self->db->do( 'DELETE FROM '.
153             $self->conf->{session_table}{_table} .
154             ' WHERE '.
155             $self->conf->{session_table}{point} .
156             ' < ' .
157             $self->db->quote($cleanup_point) );
158             }
159              
160             sub username
161             {
162 0 0   0 1   ref(my $self = shift) or croak "instance variable needed";
163 0 0         return $self->{_store}{username} if($self->{_store}{username});
164 0           return undef;
165             }
166              
167             sub uid
168             {
169 0 0   0 1   ref(my $self = shift) or croak "instance variable needed";
170 0 0         return $self->{_store}{uid} if($self->{_store}{username});
171 0           return undef;
172             }
173              
174             sub logged_in
175             {
176 0 0   0 1   ref(my $self = shift) or croak "instance variable needed";
177              
178 0 0         return 1 if($self->{_store}{username});
179 0 0 0       return undef unless(($self->{_store}{login_called}) && ($self->{_store}{username}));
180 0           return 0;
181             }
182              
183             sub groups
184             {
185 0 0   0 1   ref(my $self = shift) or croak "instance variable needed";
186              
187             # load groups for this user if we haven't loaded them already
188 0 0         unless (defined $self->{_store}{_groups})
189             {
190 0           $self->_load_groups();
191             }
192              
193 0           my @return_array;
194              
195 0           foreach my $group (keys %{$self->{_store}{_groups}})
  0            
196             {
197 0           push(@return_array,$group); # group could be a name or gid
198             }
199              
200 0           return @return_array;
201             }
202              
203             sub _load_groups
204             {
205 0 0   0     ref(my $self = shift) or croak "instance variable needed";
206              
207 0           my $group_statement = $self->conf->{group_statement};
208             # inject uid
209 0           my $q_uid = $self->db->quote( $self->{_store}{uid} );
210 0           $group_statement =~ s/\%uid\%/$q_uid/g;
211 0 0         my $get_groups = $self->db->prepare( $group_statement )
212             or croak "Unable to prepare group select statement '$group_statement'";
213 0 0         $get_groups->execute
214             or croak "Unable to execute group select statement '$group_statement'";
215 0           while (my ($name,$gid,$accessbit) = $get_groups->fetchrow_array)
216             {
217 0           $self->{_store}{_groups}{$name} = $accessbit;
218 0           $self->{_store}{_groups}{$gid} = $accessbit;
219             }
220 0           $get_groups->finish;
221             }
222              
223             sub in_group
224             {
225 0 0   0 1   ref(my $self = shift) or croak "instance variable needed";
226              
227 0           my ($group,$rw) = @_;
228              
229 0           my $rwbit;
230 0 0 0       if ($rw && ($rw =~ /^\d+$/))
    0 0        
231             { # it's a number
232 0           $rwbit = $rw;
233             } elsif ($rw && ($rw =~ /^(r|w)/i)) {
234             # it's a name (should be either "r", "w", or "rw"
235 0 0         $rwbit += 1 if ($rw =~ /r/i);
236 0 0         $rwbit += 2 if ($rw =~ /w/i);
237             } else {
238             # just return the bits, since they didn't ask for something
239 0           $rwbit = 0;
240             }
241              
242             # load groups for this user if we haven't loaded them already
243 0 0         unless (defined $self->{_store}{_groups})
244             {
245 0           $self->_load_groups();
246             }
247              
248             # $group can be either a gid or a group name.
249             # we just make sure we don't name any of our groups w/ numbers
250 0 0         if (defined $self->{_store}{_groups}{$group})
251             { # they're in the group they asked for
252             # either return the accessbits,
253             # or true/false if they specified a $rw bit
254 0 0         if ($rwbit)
255             { # we check the access bit in here (using bitwise AND)
256 0 0         warn "in_group(G[$group] rw[$rwbit])\n\tstored rwbit[".$self->{_store}{_groups}{$group}."]\n\tRV[".(($self->{_store}{_groups}{$group} & $rwbit) == $rwbit)."]\n" if $self->debug();
257 0           return (($self->{_store}{_groups}{$group} & $rwbit) == $rwbit);
258             } else {
259             # just return the access bit
260 0           return $self->{_store}{_groups}{$group};
261             }
262             } else {
263 0           return 0; # zero is no read, no write
264             }
265             }
266              
267             sub login
268             {
269            
270 0 0   0 1   ref(my $self = shift) or croak "instance variable needed";
271 0           my ($login,$passwd) = @_;
272              
273 0           my $cgi = new CGI;
274              
275 0           my $remote_address = $ENV{REMOTE_ADDR};
276 0           $self->{_store}{login_called} = 1;
277              
278 0 0 0       if ($login && $passwd)
279             { # if neither are null, they're trying to login.
280 0           my ($uid,$local_passwd,$status) = $self->_get_user_info($login);
281              
282             # invalid login (user doesn't exist)
283 0 0         return (0,$login) unless $uid;
284             # invalid login (account is disabled)
285 0 0         return (0,$login) if &{$self->conf->{user_table}{_disabled_status}}($status);
  0            
286              
287 0           my $crypt_passwd = $self->_getcrypt($passwd);
288 0 0         if ($crypt_passwd eq $local_passwd)
289             {
290             # they're authenticated... need to update local session, set cookie ticket for them, and return "1" for logged in
291 0           my $new_ticket = $self->_ticket;
292 0           my $point = time;
293 0           $self->{_store}{username} = $login;
294 0           $self->{_store}{uid} = $uid;
295 0           $self->_set_session($login,$remote_address,$new_ticket,$point);
296 0           return (1,$login,$uid);
297             } else {
298 0           return (0,$login); # invalid login (passwd doesn't match)
299             }
300             }
301              
302 0           my $remote_login = $cgi->cookie('login');
303 0           my $remote_ticket = $cgi->cookie('ticket');
304 0 0 0       if ($remote_login && $remote_ticket)
305             { # they've logged in before (or are spoofing)
306 0 0         my $get_ticket = $self->db->prepare(
307             'SELECT '.
308             $self->conf->{session_table}{ticket} .', '.
309             $self->conf->{session_table}{point} .
310             ' FROM '.
311             $self->conf->{session_table}{_table} .
312             ' WHERE '.
313             $self->conf->{session_table}{login} .' = '.
314             $self->db->quote($remote_login) .
315             ' AND '.
316             $self->conf->{session_table}{address} .' = '.
317             $self->db->quote($remote_address)
318             ) or croak "Unable to prepare get_ticket statement";
319 0 0         $get_ticket->execute()
320             or croak "Unable to execute get_ticket statement";
321            
322 0           my ($local_ticket,$local_point) = $get_ticket->fetchrow_array();
323 0           $get_ticket->finish;
324              
325 0           my $point = time;
326 0 0 0       if ($local_ticket && ($remote_ticket eq $local_ticket))
327             {
328 0 0         if ($local_point > ($point - $self->expire_seconds()))
329             { # valid ticket, continue sesson
330             # keep using existing ticket, update point on it
331             # set remote cookie's (so they don't time out)
332             # return logged in signal
333              
334             # make sure they're not disabled
335 0           my ($uid,$local_passwd,$status) = $self->_get_user_info($remote_login);
336              
337 0 0         return (0,$remote_login) if &{$self->conf->{user_table}{_disabled_status}}($status);
  0            
338              
339 0           my $point = time;
340 0           $self->_set_session($remote_login,$remote_address,$local_ticket,$point);
341 0           $self->{_store}{username} = $remote_login;
342 0           $self->{_store}{uid} = $uid;
343 0           return (1,$remote_login,$uid);
344             } else {
345             # login has expired
346 0           return (-1,$remote_login); #login expired
347             }
348             } else {
349             # invalid ticket (username cookie matched, ticket cookie didn't)
350 0           return (0,$remote_login); # invalid login
351             }
352             } else {
353             # didn't try to login, and no cookies set
354 0           return (0,0);
355             }
356             }
357              
358             sub conf
359             {
360 0 0   0 1   ref(my $self = shift) or croak "instance variable needed";
361 0 0         if (@_)
362             {
363 0           $self->{_conf} = $_[0];
364 0           return $self->{_conf};
365             } else {
366 0           return $self->{_conf};
367             }
368 0           return $self->{_conf};
369             }
370              
371             sub _get_user_info
372             {
373 0 0   0     ref(my $self = shift) or croak "instance variable needed";
374 0           my $login = shift;
375 0 0         my $get_user_info = $self->db->prepare(
376             'SELECT '.
377             $self->conf->{user_table}{uid} .', '.
378             $self->conf->{user_table}{passwd} .', '.
379             $self->conf->{user_table}{status} .
380             ' FROM '.
381             $self->conf->{user_table}{_table} .
382             ' WHERE '.
383             $self->conf->{user_table}{login} . ' = ' .
384             $self->db->quote($login) )
385             or croak "Unable to prepare get_user_info statement";
386 0 0         $get_user_info->execute
387             or croak "Unable to execute get_user_info statement";
388 0           my ($uid,$local_passwd,$status) = $get_user_info->fetchrow_array();
389 0           $get_user_info->finish;
390 0           return ($uid,$local_passwd,$status);
391             }
392              
393             sub logout
394             {
395 0 0   0 1   ref(my $self = shift) or croak "instance variable needed";
396              
397 0           my $cgi = new CGI;
398 0   0       my $login = $self->username() || $cgi->cookie('login');
399 0           my $remote_address = $ENV{REMOTE_ADDR};
400 0 0 0       if ($login && $remote_address)
401             {
402 0           $self->_set_session($login,$remote_address,'*',0);
403             }
404             # clear out the stored data
405 0           $self->{_store}{username} = '';
406 0           $self->{_store}{uid} = '';
407             # leave _groups hash ref so that we don't try to reload them
408             # but clear all access bits, removing the users access
409 0           foreach my $group (keys %{$self->{_store}{_groups}})
  0            
410             {
411 0           $self->{_store}{_groups}{$group} = '0';
412             }
413             }
414              
415             sub _set_cookie
416             {
417 0 0   0     ref(my $self = shift) or croak "instance variable needed";
418              
419 0           my ($login,$ticket,$point) = @_;
420 0           my ($login_cookie,$ticket_cookie);
421              
422 0           my $base_cookie = '; domain=' . $self->cookie_domain();
423 0 0         if ($point == 0)
424             { # if they hit logout, then try to expire their local cookie
425 0           $base_cookie .= '; max-age=0';
426             } else {
427 0           $base_cookie .= '; max-age=' . $self->expire_seconds();
428             }
429 0           $base_cookie .= '; path=/';
430 0           $base_cookie .= '; version=1';
431              
432 0           print 'Set-Cookie: login=' . $login . $base_cookie . "\n";
433 0           print 'Set-Cookie: ticket=' . $ticket . $base_cookie . "\n";
434             }
435              
436             sub _set_session
437             {
438 0 0   0     ref(my $self = shift) or croak "instance variable needed";
439              
440 0           my ($login,$address,$ticket,$point) = @_;
441              
442 0           $self->_set_cookie($login,$ticket,$point);
443              
444             # set local session
445 0 0         my $get_ticket = $self->db->prepare(
446             'SELECT '.
447             $self->conf->{session_table}{ticket} .
448             ' FROM '.
449             $self->conf->{session_table}{_table} .
450             ' WHERE '.
451             $self->conf->{session_table}{login} . ' = '.
452             $self->db->quote($login) .
453             ' AND '.
454             $self->conf->{session_table}{address} . ' = '.
455             $self->db->quote($address) )
456             or croak "Unable to prepare get_ticket statement";
457 0 0         $get_ticket->execute()
458             or croak "Unable to execute get_ticket statement";
459 0           my ($local_ticket) = $get_ticket->fetchrow_array();
460 0           $get_ticket->finish;
461            
462 0 0         if ($local_ticket)
463             { # a session has already been stored for this user/addy
464 0 0         $self->db->do(
465             'UPDATE '.
466             $self->conf->{session_table}{_table} .
467             ' SET '.
468             $self->conf->{session_table}{ticket} .' = '.
469             $self->db->quote($ticket) .', '.
470             $self->conf->{session_table}{point} .' = '.
471             $self->db->quote($point) .
472             ' WHERE '.
473             $self->conf->{session_table}{login} .' = '.
474             $self->db->quote($login) .
475             ' AND '.
476             $self->conf->{session_table}{address} .' = '.
477             $self->db->quote($address) )
478             or croak "Unable to update session table for login[$login] address[$address]";
479             } else {
480             # set a new local session
481 0 0         $self->db->do(
482             'INSERT INTO '.
483             $self->conf->{session_table}{_table} .
484             ' ('.
485             $self->conf->{session_table}{login} .', '.
486             $self->conf->{session_table}{address} .', '.
487             $self->conf->{session_table}{ticket} .', '.
488             $self->conf->{session_table}{point} .
489             ') VALUES ('.
490             $self->db->quote($login) .', '.
491             $self->db->quote($address) .', '.
492             $self->db->quote($ticket) .', '.
493             $self->db->quote($point) .')'
494             ) or croak "Unable to insert session for login[$login] address[$address]";
495             }
496             }
497              
498             sub _ticket
499             {
500 0     0     my $length = 128;
501 0           my $ticket;
502 0           while($length-- > 0)
503             {
504 0           $ticket .= chr(rand(256));
505             }
506 0           return Digest::MD5::md5_hex($ticket);
507             }
508              
509             sub _getcrypt
510             {
511 0 0   0     ref(my $self = shift) or croak "instance variable needed";
512 0           my $pass = shift;
513 0           return &{$self->conf->{'crypt'}}($pass);
  0            
514             }
515              
516             1;
517             __END__