File Coverage

lib/PSGI/Hector/Session.pm
Criterion Covered Total %
statement 124 154 80.5
branch 21 42 50.0
condition 5 10 50.0
subroutine 27 29 93.1
pod 6 7 85.7
total 183 242 75.6


line stmt bran cond sub pod time code
1             #Session functions
2             package PSGI::Hector::Session;
3              
4             =pod
5              
6             =head1 NAME
7              
8             PSGI::Hector::Session - Session class
9              
10             =head1 SYNOPSIS
11              
12             my $s = $hector->getSession();
13             $s->setVar('name', 'value');
14             my $var = $s->getVar('name');
15              
16             =head1 DESCRIPTION
17              
18             Class to deal with session management.
19              
20             =head1 METHODS
21              
22             =cut
23              
24 5     5   26 use strict;
  5         7  
  5         114  
25 5     5   18 use warnings;
  5         6  
  5         95  
26 5     5   24 use Digest::MD5;
  5         5  
  5         146  
27 5     5   2461 use Data::Dumper;
  5         27346  
  5         275  
28 5     5   2022 use CGI::Simple::Cookie;
  5         20767  
  5         113  
29 5     5   27 use File::Spec;
  5         7  
  5         102  
30 5     5   1981 use Crypt::Simple;
  5         295882  
  5         32  
31             our $prefix = "HT";
32             our $path = "/tmp";
33             ##############################################################################################################
34              
35             =head2 new()
36              
37             my $session = PSGI::Hector::Session->new($hector)
38              
39             Reads or creates a new session for the visitor.
40              
41             The correct Set-Cookie header will be issued through the provided L object.
42              
43             =cut
44              
45             ##############################################################################################################################
46             sub new{ #constructor
47 2     2 1 15 my($class, $hector) = @_;
48 2         9 my $self = {
49             '_hector' => $hector,
50             'id' => undef,
51             'vars' => {}
52             };
53 2         5 bless $self, $class;
54 2         7 $self->_readOrCreate();
55 2         9 return $self;
56             }
57             #########################################################################################################################
58             sub DESTROY{
59 1     1   252 __PACKAGE__->_expire(); #remove old sessions
60             }
61             ################################################################################################################
62              
63             =head2 setVar()
64              
65             $s->setVar('name', 'value');
66              
67             Takes two arguments, first the name of the variable then the value of the variable to store.
68              
69             =cut
70              
71             ##########################################################################################################################
72             sub setVar{ #stores a variable in the session
73 4     4 1 16 my($self, $name, $value) = @_;
74 4         12 $self->_storeVar($name, $value);
75 4         9 return $self->_write();
76             }
77             ################################################################################################################
78              
79             =head2 setSecretVar()
80              
81             $s->setSecretVar('name', 'value');
82              
83             Takes two arguments, first the name of the variable then the value of the variable to store.
84              
85             The value is encrypted before being stored.
86              
87             =cut
88              
89             #############################################################
90             sub setSecretVar{
91 0     0 1 0 my($self, $name, $value) = @_;
92 0         0 my $encrypted = encrypt($value);
93 0         0 $self->setVar($name, $encrypted);
94             }
95             ##########################################################################################################################
96              
97             =head2 getVar()
98              
99             my $value = $s->getVar('name')
100              
101             Retrieves the value which corresponds to the given variable name.
102              
103             =cut
104              
105             ##########################################################################################################################
106             sub getVar{ #gets a stored variable from the session
107 2     2 1 4 my($self, $name) = @_;
108 2 50       6 if(defined($self->{'vars'}->{$name})){
109 2         5 return $self->{'vars'}->{$name};
110             }
111 0         0 return undef;
112             }
113             ##########################################################################################################################
114              
115             =head2 getSecretVar()
116              
117             my $value = $s->getSecretVar('name')
118              
119             Retrieves the value which corresponds to the given variable name.
120              
121             The value is decrypted before being returned.
122              
123             =cut
124              
125             ###########################################################
126             sub getSecretVar{
127 0     0 1 0 my($self, $name) = @_;
128 0         0 my $encrypted = $self->getVar($name);
129 0         0 decrypt($encrypted);
130             }
131             ###########################################################################################################################
132             sub getId{ #returns the session id
133 9     9 0 25 shift->{'id'};
134             }
135             ###########################################################################################
136              
137             =pod
138              
139             =head2 delete()
140              
141             Remove the current session from memory, disk and expire it in the browser.
142              
143             =cut
144              
145             ###########################################################################################
146             sub delete{ #remove a session
147 1     1 1 246 my($self) = shift;
148 1         3 my $sessionId = $self->getId();
149 1 50       2 die("Session ID invalid: $sessionId") unless $self->_isIdValid($sessionId);
150              
151 1         2 my $path = $self->_getPath();
152 1         9 my $sessionFile = File::Spec->catfile($path, $sessionId);
153 1 50       64 die("Could not delete session") unless unlink($sessionFile);
154              
155 1         18 $self->_getHector()->getLog()->log("Deleted session: $sessionId", 'debug');
156 1         6 my $cookie = $self->_setCookie(VALUE => $sessionId, EXPIRE => 'now');
157 1         70 my $response = $self->_getHector()->getResponse();
158 1         4 $response->header("Set-Cookie" => $cookie);
159 1         47 $self = undef; #destroy this object
160 1         4 return 1;
161             }
162             ###############################################################################################################
163             #private class method
164             ###############################################################################################################
165             sub _getHector{
166 16     16   46 shift->{'_hector'};
167             }
168             ###########################################################################################################################
169             sub _setId{
170 2     2   5 my($self, $id) = @_;
171 2         3 $self->{'id'} = $id; #save the id
172 2         4 return 1;
173             }
174             ###############################################################################################################
175             sub _setCookie{
176 3     3   10 my($self, %options) = @_;
177 3         5 my $secure = 0;
178 3         7 my $hector = $self->_getHector();
179 3         9 my $env = $hector->getEnv();
180 3 50       8 if(exists($env->{'HTTPS'})){ #use secure cookies if running on ssl
181 0         0 $secure = 1;
182             }
183             my $cookie = CGI::Simple::Cookie->new(
184             -name => 'SESSION',
185             -value => $options{'VALUE'} || undef,
186 3   50     35 -expires => $options{'EXPIRE'} || undef,
      100        
187             -httponly => 1,
188             -secure => $secure
189             );
190 3 50       617 die("Can't create cookie") unless $cookie;
191            
192 3         482 return $cookie->as_string();
193             }
194             ##############################################################################################################
195             sub _expire{ #remove old session files
196 1     1   2 my $self = shift;
197 1         3 my $path = $self->_getPath();
198 1 50       32 if(opendir(COOKIES, $path)){
199 1         71 my @sessions = readdir(COOKIES);
200 1         4 my $expire = (time - 86400);
201 1         2 foreach my $id (@sessions){ #check each of the cookies
202 5 100       9 if($self->_isIdValid($id)){ #found a cookie file
203 1         10 my $sessionFile = File::Spec->catfile($path, $id);
204 1         16 my @stat = stat($sessionFile);
205 1 50 33     8 if(defined($stat[9]) && $stat[9] < $expire){ #cookie is more than a day old, so remove it
206 0         0 unlink $sessionFile;
207             }
208             }
209             }
210 1         135 closedir(COOKIES);
211             }
212             }
213             ############################################################################################################
214             #private methods
215             #########################################################################################################################
216             sub _validate{ #runs the defined sub to see if this sesion is validate
217 2     2   831 my $self = shift;
218 2         7 my $sessionIp = $self->getVar('remoteIp');
219 2 50       3 unless($sessionIp){
220 0         0 $self->_getHector()->getLog()->log("Session has no remote IP", 'debug');
221 0         0 return 0;
222             }
223 2         3 my $env = $self->_getHector()->getEnv();
224 2 100       4 if($sessionIp ne $env->{'REMOTE_ADDR'}){
225 1         3 $self->_getHector()->getLog()->log("Session " . $sessionIp . " <> " . $env->{'REMOTE_ADDR'}, 'debug');
226 1         5 return 0;
227             }
228 1         6 return 1;
229             }
230             ##############################################################################################################
231             sub _create{ #creates a server-side cookie for the session
232 2     2   4 my $self = shift;
233 2         6 my $sessionId = time() * $$; #time in seconds * process id
234 2         9 my $ctx = Digest::MD5->new;
235 2         9 $ctx->add($sessionId);
236 2         7 $sessionId = $self->_getPrefix() . $ctx->hexdigest;
237 2         8 $self->_setId($sessionId); #remember the session id
238 2         3 my $env = $self->_getHector()->getEnv();
239             #set some initial values
240 2         10 $self->setVar('remoteIp', $env->{'REMOTE_ADDR'});
241 2         8 $self->setVar('scriptPath', $env->{'SCRIPT_NAME'});
242 2         8 my $cookie = $self->_setCookie(VALUE => $self->getId());
243 2         154 my $response = $self->_getHector()->getResponse();
244 2         21 $response->header("Set-Cookie" => $cookie);
245 2         192 return 1;
246             }
247             ##############################################################################################################
248             sub _read{ #read an existing session
249 2     2   2 my $self = shift;
250 2         3 my $result = 0;
251 2         6 my $sessionId = $self->_getHector()->getRequest()->getCookie("SESSION"); #get the session id from the browser
252 2 50       28 return 0 unless defined($sessionId); #got a sessionid of some sort
253              
254 0 0       0 return 0 unless $self->_isIdValid($sessionId); #filename valid
255              
256 0         0 my $path = $self->_getPath();
257 0         0 my $sessionFile = File::Spec->catfile($path, $sessionId);
258 0 0       0 return 0 unless open(SSIDE, "<", $sessionFile); #try to open the session file
259              
260 0         0 my $contents = "";
261 0         0 while(){ #read each line of the file
262 0         0 $contents .= $_;
263             }
264 0         0 close(SSIDE);
265 0 0       0 unless($contents =~ m/^(\$VAR1 = \{.+\};)$/m){ #check session contents
266 0         0 $self->_getHector()->getLog()->log('Session contents invalid', 'warn');
267 0         0 return 0;
268             }
269            
270 0         0 my $validContents = $1; #untaint variable
271 0         0 my $VAR1; #the session contents var
272             {
273 0         0 eval $validContents;
  0         0  
274             }
275 0         0 $self->{'vars'} = $VAR1;
276 0         0 $self->_setId($sessionId); #remember the session id
277 0         0 return 1;
278             }
279             ###########################################################################################
280             sub _write{ #writes a server-side cookie for the session
281 4     4   4 my $self = shift;
282 4         9 my $sessionId = $self->getId();
283 4 50       10 die('Session ID invalid') unless $self->_isIdValid($sessionId); #filename valid
284              
285 4         12 my $sessionFile = File::Spec->catfile($self->_getPath(), $sessionId);
286 4 50       377 die("Cant write session: $!") unless open(SSIDE, ">", $sessionFile);
287            
288 4         13 $Data::Dumper::Freezer = 'freeze';
289 4         7 $Data::Dumper::Toaster = 'toast';
290 4         4 $Data::Dumper::Indent = 0; #turn off formatting
291 4         17 my $dump = Dumper $self->{'vars'};
292 4 50       223 if($dump){ #if we have any data
293 4         43 print SSIDE $dump;
294             }
295 4         250 close(SSIDE);
296 4         16 return 1;
297             }
298             ##########################################################################################################################
299             sub _storeVar{ #stores a variable in the session
300 4     4   7 my($self, $name, $value) = @_;
301 4 100       9 if(!defined($value)){ #remove the var
302 1 50       3 if($self->{'vars'}){
303 1         1 my %vars = %{$self->{'vars'}};
  1         9  
304 1         2 delete $vars{$name};
305 1         3 $self->{'vars'} = \%vars;
306             }
307             }
308             else{ #update/create a var
309 3         7 $self->{'vars'}->{$name} = $value; #store for later
310             }
311 4         5 return 1;
312             }
313             #####################################################################################################################
314             sub _getPrefix{ #this should be a config option
315 12     12   30 return $prefix;
316             }
317             #####################################################################################################################
318             sub _getPath{ #this should be a config option
319 6     6   53 return $path;
320             }
321             #####################################################################################################################
322             sub _readOrCreate{
323 2     2   3 my $self = shift;
324 2 50 33     7 if($self->_read() && $self->_validate()){
    50          
325 0         0 $self->_getHector()->getLog()->log("Existing session: " . $self->getId(), 'debug');
326             }
327             elsif($self->_create()){ #start a new session
328 2         6 $self->_getHector()->getLog()->log("Created new session: " . $self->getId(), 'debug');
329             }
330             }
331             #######################################################################################################
332             sub _isIdValid{
333 10     10   21 my($self, $id) = @_;
334 10         16 my $prefix = $self->_getPrefix();
335 10         90 $id =~ m/^($prefix[a-f0-9]+)$/;
336             }
337             #####################################################################################################################
338              
339             =pod
340              
341             =head1 Notes
342              
343             =head1 Author
344              
345             MacGyveR
346              
347             Development questions, bug reports, and patches are welcome to the above address
348              
349             =head1 See Also
350              
351             =head1 Copyright
352              
353             Copyright (c) 2020 MacGyveR. All rights reserved.
354              
355             This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
356              
357             =cut
358              
359             ##########################################
360             return 1;
361       5     END {}