File Coverage

blib/lib/MVC/Neaf/X/Session.pm
Criterion Covered Total %
statement 40 40 100.0
branch 4 6 66.6
condition 4 8 50.0
subroutine 13 13 100.0
pod 6 6 100.0
total 67 73 91.7


line stmt bran cond sub pod time code
1             package MVC::Neaf::X::Session;
2              
3 10     10   75907 use strict;
  10         30  
  10         318  
4 10     10   55 use warnings;
  10         18  
  10         493  
5             our $VERSION = '0.29';
6              
7             =head1 NAME
8              
9             MVC::Neaf::X::Session - Session engine base class for Not Even A Framework
10              
11             =head1 DESCRIPTION
12              
13             A framework, even a toy one, is incomplete until it can handle user sessions.
14              
15             This class offers managing sessions via a cookie ("session" by default)
16             plus a user-defined backend storage mechanism.
17              
18             Whatever is stored in the session, stays in the session - until it's deleted.
19              
20             Within the application, session is available through Request methods
21             session(), save_session(), and delete_session().
22             During the setup phase, MVC::Neaf->set_session_handler( $engine )
23             must be called in order to make use of those.
24              
25             This class is base class for such $engine.
26              
27             To actually manage sessions, it MUST be subclassed with methods
28             save_session() and load_session() implemented.
29             For a working implementation, please see L.
30              
31             This module's interface is still under development and details MAY
32             change in the future.
33              
34             =head1 SINOPSYS
35              
36             use MVC::Neaf;
37             use MVC::Neaf::X::Session;
38              
39             # somewhere in the beginning
40             {
41             package My::Session;
42              
43             sub save_session {
44             my ($self, $id, $data) = @_;
45             $self->{data}{ $id } = $data;
46             return { id => $id };
47             };
48              
49             sub load_session {
50             my ($self, $id) = @_;
51             return { data => $self->{data}{ $id } };
52             };
53             };
54             MVC::Neaf->set_session_handler( My::Session->new );
55              
56             # somewhere in the controller
57             sub {
58             my $req = shift;
59              
60             $req->session; # {} 1st time, { user => ... } later on
61             $req->session->{user} = $user;
62             $req->save_session;
63             };
64              
65             This of course is only going to work as a standalone application server
66             (plackup, twiggy...), but not CGI or Apache/mod_perl.
67              
68             =head1 METHODS
69              
70             =cut
71              
72 10     10   64 use Digest::MD5;
  10         22  
  10         361  
73 10     10   2184 use Time::HiRes qw(gettimeofday);
  10         5775  
  10         71  
74 10     10   4021 use Sys::Hostname qw(hostname);
  10         4364  
  10         650  
75 10     10   516 use MVC::Neaf::Util qw(encode_b64);
  10         33  
  10         469  
76              
77 10     10   66 use parent qw(MVC::Neaf::X);
  10         34  
  10         80  
78              
79             =head2 new( %options )
80              
81             %options may include
82              
83             =over
84              
85             =item * session_ttl, expire - the lifetime of session.
86             Default is 24 hours.
87              
88             =back
89              
90             =cut
91              
92             sub new {
93 11     11 1 290 my ($class, %opt) = @_;
94              
95 11   50     103 $opt{session_ttl} ||= delete $opt{expire} || 24*60*60;
      66        
96              
97 11         101 $class->SUPER::new( %opt );
98             };
99              
100             =head2 session_id_regex()
101              
102             This is supposed to be a constant regular expression
103             compatible with whatever get_session_id generates.
104              
105             If none given, a sane default is supplied.
106              
107             =cut
108              
109             sub session_id_regex {
110 6     6 1 38 return qr([A-Za-z_\d\.\/\?\-\@+=~]+);
111             };
112              
113             =head2 get_session_id( [$user_salt] )
114              
115             Generate a new, shiny, unique, unpredictable session id.
116             Id is base64-encoded.
117              
118             The default is using two rounds of md5 with time, process id, hostname,
119             and random salt. Should be unique and reasonably hard to guess.
120              
121             If argument is given, it's also added to the mix.
122              
123             Set $MVC::Neaf::X::Session::Hash to other function (e.g. Digest::SHA::sha224)
124             if md5 is not secure enough.
125              
126             Set $MVC::Neaf::X::Session::Host to something unique if you know better.
127             Default is hostname.
128              
129             Set $MVC::Neaf::X::Session::Truncate to the desired length
130             (e.g. if length constraint in database).
131             Default (0) means return however many chars are generated by hash+base64.
132              
133             =cut
134              
135             # Premature optimisation at its best.
136             # Should be more or less secure and unique though.
137             my $max = 2*1024*1024*1024;
138             my $count = 0;
139             my $old_rand = 0;
140             my $old_mix = '';
141             our $Host = hostname() || '';
142             our $Hash = \&Digest::MD5::md5;
143             our $Truncate;
144              
145             sub get_session_id {
146 1013     1013 1 3366 my ($self, $salt) = @_;
147              
148 1013 100       1853 $count = $max
149             unless $count--;
150 1013         2123 my $rand = int ( rand() * $max );
151 1013         2053 my ($time, $ms) = gettimeofday();
152 1013 50       1859 $salt = '' unless defined $salt;
153              
154             # using old entropy means attacker will have to guess ALL previous sessions
155 1013         4287 $old_mix = $Hash->(pack "La*a*a*a*LLLLa*L"
156             , $rand, $old_mix, "#"
157             , $Host, '#', $$, $time, $ms, $count
158             , $salt, $old_rand);
159              
160             # salt before second round of hashing
161             # public data (session_id) should NOT be used for generation
162 1013         1818 $old_rand = int (rand() * $max );
163 1013         3143 my $ret = encode_b64( $Hash->( pack "a*L", $old_mix, $old_rand ) );
164 1013         10339 $ret =~ s/[\s=]+//gs;
165 1013 50 33     1915 $ret = substr( $ret, 0, $Truncate )
166             if $Truncate and $Truncate < length $ret;
167 1013         3232 return $ret;
168             };
169              
170             # finally, bootstrap the session generator at startap
171             get_session_id();
172              
173             =head2 session_ttl()
174              
175             Return session ttl.
176              
177             =cut
178              
179             sub session_ttl {
180 19     19 1 36 my $self = shift;
181 19         98 return $self->{session_ttl};
182             };
183              
184             =head2 create_session()
185              
186             Create a new session. The default is to return an empty hash.
187              
188             =cut
189              
190 4     4 1 19 sub create_session { return {} };
191              
192             =head2 save_session( $id, $data )
193              
194             Save session data in the storage.
195              
196             This method MUST be implemented in specific session driver class.
197              
198             It MUST return a hashref with the following fields:
199              
200             =over
201              
202             =item * id - the id of session (either supplied, or a new one).
203             If this value is absent or false, saving is considered unsuccessful.
204              
205             =item * expire - the expiration time of the session as Unix time.
206             This is optional.
207              
208             =back
209              
210             =cut
211              
212             =head2 load_session( $id )
213              
214             Return session data from the storage.
215              
216             This MUST be implemented in specific session driver class.
217              
218             It MUST return either false, or a hashref with the following fields:
219              
220             =over
221              
222             =item * data - the session data that was passed to corresponding save_session()
223             call. If absent or false, loading is considered unsuccessful.
224              
225             =item * id - if present, this means that session has to be refreshed.
226             The session cookie will be sent again to the user.
227              
228             =item * expire - if id present, this would set new session expiration date.
229              
230             =back
231              
232             =cut
233              
234             =head2 delete_session( $id )
235              
236             Remove session from storage.
237              
238             The default is do nothing and wait for session data to rot by itself.
239              
240             B It is usually a good idea to cleanup session storage
241             from time to time since some users may go away without logging out
242             (cleaned cookies, laptop eaten by crocodiles etc).
243              
244             =cut
245              
246 1     1 1 4 sub delete_session { return };
247              
248             =head1 LICENSE AND COPYRIGHT
249              
250             This module is part of L suite.
251              
252             Copyright 2016-2023 Konstantin S. Uvarin C.
253              
254             This program is free software; you can redistribute it and/or modify it
255             under the terms of either: the GNU General Public License as published
256             by the Free Software Foundation; or the Artistic License.
257              
258             See L for more information.
259              
260             =cut
261              
262             1;