File Coverage

blib/lib/Dancer2/Core/Role/SessionFactory.pm
Criterion Covered Total %
statement 96 103 93.2
branch 20 30 66.6
condition 4 9 44.4
subroutine 20 22 90.9
pod 10 12 83.3
total 150 176 85.2


line stmt bran cond sub pod time code
1             # ABSTRACT: Role for session factories
2             $Dancer2::Core::Role::SessionFactory::VERSION = '0.400000';
3             use Moo::Role;
4 113     113   81025 with 'Dancer2::Core::Role::Engine';
  113         275  
  113         885  
5              
6             use Carp 'croak';
7 113     113   50508 use Dancer2::Core::Session;
  113         257  
  113         4831  
8 113     113   43650 use Dancer2::Core::Types;
  113         359  
  113         3676  
9 113     113   810 use Digest::SHA 'sha1';
  113         249  
  113         1081  
10 113     113   987996 use List::Util 'shuffle';
  113         314158  
  113         9756  
11 113     113   963 use MIME::Base64 'encode_base64url';
  113         267  
  113         7088  
12 113     113   45993 use Module::Runtime 'require_module';
  113         63486  
  113         6543  
13 113     113   1623 use Ref::Util qw< is_ref is_arrayref is_hashref >;
  113         3143  
  113         1126  
14 113     113   6490  
  113         1141  
  113         162346  
15             qw/
16 20335     20335 0 42400 engine.session.before_retrieve
17             engine.session.after_retrieve
18 34     34 0 2312  
19             engine.session.before_create
20             engine.session.after_create
21              
22             engine.session.before_change_id
23             engine.session.after_change_id
24              
25             engine.session.before_destroy
26             engine.session.after_destroy
27              
28             engine.session.before_flush
29             engine.session.after_flush
30             /;
31             }
32              
33             'SessionFactory';
34             } # XXX vs 'Session'? Unused, so I can't tell -- xdg
35              
36             has log_cb => (
37 0     0   0 is => 'ro',
38             isa => CodeRef,
39             default => sub { sub {1} },
40             );
41              
42             has cookie_name => (
43             is => 'ro',
44             isa => Str,
45             default => sub {'dancer.session'},
46             );
47              
48             has cookie_domain => (
49             is => 'ro',
50             isa => Str,
51             predicate => 1,
52             );
53              
54             has cookie_path => (
55             is => 'ro',
56             isa => Str,
57             default => sub {"/"},
58             );
59              
60             has cookie_duration => (
61             is => 'ro',
62             isa => Str,
63             predicate => 1,
64             );
65              
66             has session_duration => (
67             is => 'ro',
68             isa => Num,
69             predicate => 1,
70             );
71              
72             has is_secure => (
73             is => 'rw',
74             isa => Bool,
75             default => sub {0},
76             );
77              
78             has is_http_only => (
79             is => 'rw',
80             isa => Bool,
81             default => sub {1},
82             );
83              
84             has cookie_same_site => (
85             is => 'ro',
86             isa => Str,
87             predicate => 1,
88             coerce => sub { ucfirst $_[0] },
89             );
90              
91             my ($self) = @_;
92              
93             my %args = ( id => $self->generate_id, );
94              
95             $args{expires} = $self->cookie_duration
96 10046     10046 1 222235 if $self->has_cookie_duration;
97              
98 10046         17159 my $session = Dancer2::Core::Session->new(%args);
99              
100 10046 100       91523 $self->execute_hook( 'engine.session.before_create', $session );
101              
102             # XXX why do we _flush now? Seems unnecessary -- xdg, 2013-03-03
103 10046         151766 eval { $self->_flush( $session->id, $session->data ) };
104             croak "Unable to create a new session: $@"
105 10046         173505 if $@;
106              
107             $self->execute_hook( 'engine.session.after_create', $session );
108 10046         65673 return $session;
  10046         128275  
109 10046 50       18114 }
110              
111             {
112 10046         22172 my $COUNTER = 0;
113 10046         71871 my $CPRNG_AVAIL = eval { require_module('Math::Random::ISAAC::XS'); 1; } &&
114             eval { require_module('Crypt::URandom'); 1; };
115              
116             # don't initialize until generate_id is called so the ISAAC algorithm
117             # is seeded after any pre-forking
118             my $CPRNG;
119              
120             # prepend epoch seconds so session ID is roughly monotonic
121             my ($self) = @_;
122              
123             if ($CPRNG_AVAIL) {
124             $CPRNG ||= Math::Random::ISAAC::XS->new(
125             map { unpack( "N", Crypt::URandom::urandom(4) ) } 1 .. 256 );
126              
127 10047     10047 1 15264 # include $$ to ensure $CPRNG wasn't forked by accident
128             return encode_base64url(
129 10047 50       17107 pack(
130             "N6",
131 10047   66     16701 time, $$, $CPRNG->irand,
  3840         77914  
132             $CPRNG->irand, $CPRNG->irand, $CPRNG->irand
133             )
134 10047         55208 );
135             }
136             else {
137             my $seed = (
138             rand(1_000_000_000) # a random number
139             . __FILE__ # the absolute path as a secret key
140             . $COUNTER++ # impossible to have two consecutive dups
141             . $$ # the process ID as another private constant
142             . "$self" # the instance's memory address for more entropy
143 0         0 . join( '', shuffle( 'a' .. 'z', 'A' .. 'Z', 0 .. 9 ) )
144              
145             # a shuffled list of 62 chars, another random component
146             );
147             return encode_base64url( pack( "Na*", time, sha1($seed) ) );
148             }
149              
150             }
151             }
152              
153 0         0 my ($self, $id) = @_;
154             return $id =~ m/^[A-Za-z0-9_\-~]+$/;
155             }
156              
157             requires '_retrieve';
158              
159             my ( $self, %params ) = @_;
160 53     53 1 161 my $id = $params{id};
161 53         620  
162             $self->execute_hook( 'engine.session.before_retrieve', $id );
163              
164             my $data;
165             # validate format of session id before attempt to retrieve
166             my $rc = eval {
167 54     54 1 604 $self->validate_id($id) && ( $data = $self->_retrieve($id) );
168 54         142 };
169             croak "Unable to retrieve session with id '$id'"
170 54         221 if ! $rc;
171              
172 54         416 my %args = ( id => $id, );
173              
174 54         101 $args{data} = $data
175 54 100       199 if $data and is_hashref($data);
176              
177 54 100       766 $args{expires} = $self->cookie_duration
178             if $self->has_cookie_duration;
179              
180 51         177 my $session = Dancer2::Core::Session->new(%args);
181              
182 51 50 33     380 $self->execute_hook( 'engine.session.after_retrieve', $session );
183             return $session;
184             }
185 51 100       265  
186             # XXX eventually we could perhaps require '_change_id'?
187              
188 51         1305 my ( $self, %params ) = @_;
189             my $session = $params{session};
190 51         1278 my $old_id = $session->id;
191 51         524  
192             $self->execute_hook( 'engine.session.before_change_id', $old_id );
193              
194             my $new_id = $self->generate_id;
195             $session->id( $new_id );
196              
197 3     3 1 12 eval { $self->_change_id( $old_id, $new_id ) };
198 3         8 croak "Unable to change session id for session with id $old_id: $@"
199 3         49 if $@;
200              
201 3         25 $self->execute_hook( 'engine.session.after_change_id', $new_id );
202             }
203 3         32  
204 3         85 requires '_destroy';
205              
206 3         84 my ( $self, %params ) = @_;
  3         14  
207 3 50       135 my $id = $params{id};
208             $self->execute_hook( 'engine.session.before_destroy', $id );
209              
210 3         13 eval { $self->_destroy($id) };
211             croak "Unable to destroy session with id '$id': $@"
212             if $@;
213              
214             $self->execute_hook( 'engine.session.after_destroy', $id );
215             return $id;
216 17     17 1 175 }
217 17         52  
218 17         71 requires '_flush';
219              
220 17         75 my ( $self, %params ) = @_;
  17         87  
221 17 50       94 my $session = $params{session};
222             $self->execute_hook( 'engine.session.before_flush', $session );
223              
224 17         68 eval { $self->_flush( $session->id, $session->data ) };
225 17         163 croak "Unable to flush session: $@"
226             if $@;
227              
228             $self->execute_hook( 'engine.session.after_flush', $session );
229             return $session->id;
230             }
231 49     49 1 1081  
232 49         1728 my ( $self, %params ) = @_;
233 49         213 $params{response}->push_header(
234             'Set-Cookie',
235 49         366 $self->cookie( session => $params{session} )->to_header
  49         770  
236 49 50       337 );
237             }
238              
239 49         2100 my ( $self, %params ) = @_;
240 49         963 my $session = $params{session};
241             croak "cookie() requires a valid 'session' parameter"
242             unless is_ref($session) && $session->isa("Dancer2::Core::Session");
243              
244 88     88 1 399 my %cookie = (
245             value => $session->id,
246             name => $self->cookie_name,
247 88         467 path => $self->cookie_path,
248             secure => $self->is_secure,
249             http_only => $self->is_http_only,
250             );
251              
252 88     88 1 284 $cookie{same_site} = $self->cookie_same_site
253 88         205 if $self->has_cookie_same_site;
254 88 50 33     701  
255             $cookie{domain} = $self->cookie_domain
256             if $self->has_cookie_domain;
257 88         1507  
258             if ( my $expires = $session->expires ) {
259             $cookie{expires} = $expires;
260             }
261              
262             return Dancer2::Core::Cookie->new(%cookie);
263             }
264              
265 88 100       4371 requires '_sessions';
266              
267             my ($self) = @_;
268 88 50       324 my $sessions = $self->_sessions;
269              
270             croak "_sessions() should return an array ref"
271 88 100       1375 unless is_arrayref($sessions);
272 14         105  
273             return $sessions;
274             }
275 88         2039  
276             1;
277              
278              
279             =pod
280              
281 0     0 1   =encoding UTF-8
282 0            
283             =head1 NAME
284 0 0          
285             Dancer2::Core::Role::SessionFactory - Role for session factories
286              
287 0           =head1 VERSION
288              
289             version 0.400000
290              
291             =head1 DESCRIPTION
292              
293             Any class that consumes this role will be able to store, create, retrieve and
294             destroy session objects.
295              
296             The default values for attributes can be overridden in your Dancer2
297             configuration. See L<Dancer2::Config/Session-engine>.
298              
299             =head1 ATTRIBUTES
300              
301             =head2 cookie_name
302              
303             The name of the cookie to create for storing the session key
304              
305             Defaults to C<dancer.session>
306              
307             =head2 cookie_domain
308              
309             The domain of the cookie to create for storing the session key.
310             Defaults to the empty string and is unused as a result.
311              
312             =head2 cookie_path
313              
314             The path of the cookie to create for storing the session key.
315             Defaults to "/".
316              
317             =head2 cookie_duration
318              
319             Default duration before session cookie expiration. If set, the
320             L<Dancer2::Core::Session> C<expires> attribute will be set to the current time
321             plus this duration (expression parsed by L<Dancer2::Core::Time>).
322              
323             =head2 cookie_same_site
324              
325             Restricts the session cookie to a first-party or same-site context.
326             Defaults to the empty string and is unused as a result.
327             See L<Dancer2::Core::Cookie/same_site>.
328              
329             =head2 session_duration
330              
331             Duration in seconds before sessions should expire, regardless of cookie
332             expiration. If set, then SessionFactories should use this to enforce a limit
333             on session validity.
334              
335             =head2 is_secure
336              
337             Boolean flag to tell if the session cookie is secure or not.
338              
339             Default is false.
340              
341             =head2 is_http_only
342              
343             Boolean flag to tell if the session cookie is http only.
344              
345             Default is true.
346              
347             =head1 INTERFACE
348              
349             Following is the interface provided by this role. When specified the required
350             methods to implement are described.
351              
352             =head2 create
353              
354             Create a brand new session object and store it. Returns the newly created
355             session object.
356              
357             Triggers an exception if the session is unable to be created.
358              
359             my $session = MySessionFactory->create();
360              
361             This method does not need to be implemented in the class.
362              
363             =head2 generate_id
364              
365             Returns a randomly-generated, guaranteed-unique string.
366             By default, it is a 32-character, URL-safe, Base64 encoded combination
367             of a 32 bit timestamp and a 160 bit SHA1 digest of random seed data.
368             The timestamp ensures that session IDs are generally monotonic.
369              
370             The default algorithm is not guaranteed cryptographically secure, but it's
371             still reasonably strong for general use.
372              
373             If you have installed L<Math::Random::ISAAC::XS> and L<Crypt::URandom>,
374             the seed data will be generated from a cryptographically-strong
375             random number generator.
376              
377             This method is used internally by create() to set the session ID.
378              
379             This method does not need to be implemented in the class unless an
380             alternative method for session ID generation is desired.
381              
382             =head2 validate_id
383              
384             Returns true if a session id is of the correct format, or false otherwise.
385              
386             By default, this ensures that the session ID is a string of characters
387             from the Base64 schema for "URL Applications" plus the C<~> character.
388              
389             This method does not need to be implemented in the class unless an
390             alternative set of characters for session IDs is desired.
391              
392             =head2 retrieve
393              
394             Return the session object corresponding to the session ID given. If none is
395             found, triggers an exception.
396              
397             my $session = MySessionFactory->retrieve(id => $id);
398              
399             The method C<_retrieve> must be implemented. It must take C<$id> as a single
400             argument and must return a hash reference of session data.
401              
402             =head2 change_id
403              
404             Changes the session ID of the corresponding session.
405              
406             MySessionFactory->change_id(session => $session_object);
407              
408             The method C<_change_id> must be implemented. It must take C<$old_id> and
409             C<$new_id> as arguments and change the ID from the old one to the new one
410             in the underlying session storage.
411              
412             =head2 destroy
413              
414             Purges the session object that matches the ID given. Returns the ID of the
415             destroyed session if succeeded, triggers an exception otherwise.
416              
417             MySessionFactory->destroy(id => $id);
418              
419             The C<_destroy> method must be implemented. It must take C<$id> as a single
420             argument and destroy the underlying data.
421              
422             =head2 flush
423              
424             Make sure the session object is stored in the factory's backend. This method is
425             called to notify the backend about the change in the session object.
426              
427             The Dancer application will not call flush unless the session C<is_dirty>
428             attribute is true to avoid unnecessary writes to the database when no
429             data has been modified.
430              
431             An exception is triggered if the session is unable to be updated in the backend.
432              
433             MySessionFactory->flush(session => $session);
434              
435             The C<_flush> method must be implemented. It must take two arguments: the C<$id>
436             and a hash reference of session data.
437              
438             =head2 set_cookie_header
439              
440             Sets the session cookie into the response object
441              
442             MySessionFactory->set_cookie_header(
443             response => $response,
444             session => $session,
445             destroyed => undef,
446             );
447              
448             The C<response> parameter contains a L<Dancer2::Core::Response> object.
449             The C<session> parameter contains a L<Dancer2::Core::Session> object.
450              
451             The C<destroyed> parameter is optional. If true, it indicates the
452             session was marked destroyed by the request context. The default
453             C<set_cookie_header> method doesn't need that information, but it is
454             included in case a SessionFactory must handle destroyed sessions
455             differently (such as signalling to middleware).
456              
457             =head2 cookie
458              
459             Coerce a session object into a L<Dancer2::Core::Cookie> object.
460              
461             MySessionFactory->cookie(session => $session);
462              
463             =head2 sessions
464              
465             Return a list of all session IDs stored in the backend.
466             Useful to create cleaning scripts, in conjunction with session's creation time.
467              
468             The C<_sessions> method must be implemented. It must return an array reference
469             of session IDs (or an empty array reference).
470              
471             =head1 CONFIGURATION
472              
473             If there are configuration values specific to your session factory in your config.yml or
474             environment, those will be passed to the constructor of the session factory automatically.
475             In order to accept and store them, you need to define accessors for them.
476              
477             engines:
478             session:
479             Example:
480             database_connection: "some_data"
481              
482             In your session factory:
483              
484             package Dancer2::Session::Example;
485             use Moo;
486             with "Dancer2::Core::Role::SessionFactory";
487              
488             has database_connection => ( is => "ro" );
489              
490             You need to do this for every configuration key. The ones that do not have accessors
491             defined will just go to the void.
492              
493             =head1 AUTHOR
494              
495             Dancer Core Developers
496              
497             =head1 COPYRIGHT AND LICENSE
498              
499             This software is copyright (c) 2022 by Alexis Sukrieh.
500              
501             This is free software; you can redistribute it and/or modify it under
502             the same terms as the Perl 5 programming language system itself.
503              
504             =cut