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             package Dancer2::Core::Role::SessionFactory;
2             # ABSTRACT: Role for session factories
3             $Dancer2::Core::Role::SessionFactory::VERSION = '2.0.1';
4 125     125   481558 use Moo::Role;
  125         19350  
  125         1871  
5             with 'Dancer2::Core::Role::Engine';
6              
7 125     125   78907 use Carp 'croak';
  125         734  
  125         9299  
8 125     125   73932 use Dancer2::Core::Session;
  125         612  
  125         6221  
9 125     125   1335 use Dancer2::Core::Types;
  125         598  
  125         991  
10 125     125   1978516 use Digest::SHA 'sha1';
  125         480990  
  125         14995  
11 125     125   1270 use List::Util 'shuffle';
  125         280  
  125         10957  
12 125     125   64011 use MIME::Base64 'encode_base64url';
  125         109586  
  125         10223  
13 125     125   4459 use Module::Runtime 'require_module';
  125         14396  
  125         1318  
14 125     125   13717 use Ref::Util qw< is_ref is_arrayref is_hashref >;
  125         4724  
  125         247138  
15              
16 20335     20335 0 59685 sub hook_aliases { +{} }
17             sub supported_hooks {
18 34     34 0 711 qw/
19             engine.session.before_retrieve
20             engine.session.after_retrieve
21              
22             engine.session.before_create
23             engine.session.after_create
24              
25             engine.session.before_change_id
26             engine.session.after_change_id
27              
28             engine.session.before_destroy
29             engine.session.after_destroy
30              
31             engine.session.before_flush
32             engine.session.after_flush
33             /;
34             }
35              
36             sub _build_type {
37 0     0   0 'SessionFactory';
38             } # XXX vs 'Session'? Unused, so I can't tell -- xdg
39              
40             has log_cb => (
41             is => 'ro',
42             isa => CodeRef,
43             default => sub { sub {1} },
44             );
45              
46             has cookie_name => (
47             is => 'ro',
48             isa => Str,
49             default => sub {'dancer.session'},
50             );
51              
52             has cookie_domain => (
53             is => 'ro',
54             isa => Str,
55             predicate => 1,
56             );
57              
58             has cookie_path => (
59             is => 'ro',
60             isa => Str,
61             default => sub {"/"},
62             );
63              
64             has cookie_duration => (
65             is => 'ro',
66             isa => Str,
67             predicate => 1,
68             );
69              
70             has session_duration => (
71             is => 'ro',
72             isa => Num,
73             predicate => 1,
74             );
75              
76             has is_secure => (
77             is => 'rw',
78             isa => Bool,
79             default => sub {0},
80             );
81              
82             has is_http_only => (
83             is => 'rw',
84             isa => Bool,
85             default => sub {1},
86             );
87              
88             has cookie_same_site => (
89             is => 'ro',
90             isa => Str,
91             predicate => 1,
92             coerce => sub { ucfirst $_[0] },
93             );
94              
95             sub create {
96 10046     10046 1 384380 my ($self) = @_;
97              
98 10046         31460 my %args = ( id => $self->generate_id, );
99              
100 10046 100       153395 $args{expires} = $self->cookie_duration
101             if $self->has_cookie_duration;
102              
103 10046         266423 my $session = Dancer2::Core::Session->new(%args);
104              
105 10046         292619 $self->execute_hook( 'engine.session.before_create', $session );
106              
107             # XXX why do we _flush now? Seems unnecessary -- xdg, 2013-03-03
108 10046         95395 eval { $self->_flush( $session->id, $session->data ) };
  10046         218254  
109 10046 50       30964 croak "Unable to create a new session: $@"
110             if $@;
111              
112 10046         32796 $self->execute_hook( 'engine.session.after_create', $session );
113 10046         112162 return $session;
114             }
115              
116             {
117             my $COUNTER = 0;
118             my $CPRNG_AVAIL = eval { require_module('Math::Random::ISAAC::XS'); 1; } &&
119             eval { require_module('Crypt::URandom'); 1; };
120              
121             # don't initialize until generate_id is called so the ISAAC algorithm
122             # is seeded after any pre-forking
123             my $CPRNG;
124              
125             # prepend epoch seconds so session ID is roughly monotonic
126             sub generate_id {
127 10047     10047 1 19835 my ($self) = @_;
128              
129 10047 50       22956 if ($CPRNG_AVAIL) {
130             $CPRNG ||= Math::Random::ISAAC::XS->new(
131 10047   66     26021 map { unpack( "N", Crypt::URandom::urandom(4) ) } 1 .. 256 );
  3840         78863  
132              
133             # include $$ to ensure $CPRNG wasn't forked by accident
134 10047         143773 return encode_base64url(
135             pack(
136             "N6",
137             time, $$, $CPRNG->irand,
138             $CPRNG->irand, $CPRNG->irand, $CPRNG->irand
139             )
140             );
141             }
142             else {
143 0         0 my $seed = (
144             rand(1_000_000_000) # a random number
145             . __FILE__ # the absolute path as a secret key
146             . $COUNTER++ # impossible to have two consecutive dups
147             . $$ # the process ID as another private constant
148             . "$self" # the instance's memory address for more entropy
149             . join( '', shuffle( 'a' .. 'z', 'A' .. 'Z', 0 .. 9 ) )
150              
151             # a shuffled list of 62 chars, another random component
152             );
153 0         0 return encode_base64url( pack( "Na*", time, sha1($seed) ) );
154             }
155              
156             }
157             }
158              
159             sub validate_id {
160 53     53 1 176 my ($self, $id) = @_;
161 53         667 return $id =~ m/^[A-Za-z0-9_\-~]+$/;
162             }
163              
164             requires '_retrieve';
165              
166             sub retrieve {
167 54     54 1 589 my ( $self, %params ) = @_;
168 54         149 my $id = $params{id};
169              
170 54         284 $self->execute_hook( 'engine.session.before_retrieve', $id );
171              
172 54         444 my $data;
173             # validate format of session id before attempt to retrieve
174 54         143 my $rc = eval {
175 54 100       226 $self->validate_id($id) && ( $data = $self->_retrieve($id) );
176             };
177 54 100       825 croak "Unable to retrieve session with id '$id'"
178             if ! $rc;
179              
180 51         196 my %args = ( id => $id, );
181              
182 51 50 33     431 $args{data} = $data
183             if $data and is_hashref($data);
184              
185 51 100       286 $args{expires} = $self->cookie_duration
186             if $self->has_cookie_duration;
187              
188 51         1822 my $session = Dancer2::Core::Session->new(%args);
189              
190 51         1571 $self->execute_hook( 'engine.session.after_retrieve', $session );
191 51         590 return $session;
192             }
193              
194             # XXX eventually we could perhaps require '_change_id'?
195              
196             sub change_id {
197 3     3 1 11 my ( $self, %params ) = @_;
198 3         10 my $session = $params{session};
199 3         67 my $old_id = $session->id;
200              
201 3         32 $self->execute_hook( 'engine.session.before_change_id', $old_id );
202              
203 3         26 my $new_id = $self->generate_id;
204 3         130 $session->id( $new_id );
205              
206 3         100 eval { $self->_change_id( $old_id, $new_id ) };
  3         17  
207 3 50       243 croak "Unable to change session id for session with id $old_id: $@"
208             if $@;
209              
210 3         15 $self->execute_hook( 'engine.session.after_change_id', $new_id );
211             }
212              
213             requires '_destroy';
214              
215             sub destroy {
216 17     17 1 210 my ( $self, %params ) = @_;
217 17         73 my $id = $params{id};
218 17         102 $self->execute_hook( 'engine.session.before_destroy', $id );
219              
220 17         96 eval { $self->_destroy($id) };
  17         92  
221 17 50       120 croak "Unable to destroy session with id '$id': $@"
222             if $@;
223              
224 17         75 $self->execute_hook( 'engine.session.after_destroy', $id );
225 17         180 return $id;
226             }
227              
228             requires '_flush';
229              
230             sub flush {
231 49     49 1 892 my ( $self, %params ) = @_;
232 49         128 my $session = $params{session};
233 49         322 $self->execute_hook( 'engine.session.before_flush', $session );
234              
235 49         412 eval { $self->_flush( $session->id, $session->data ) };
  49         984  
236 49 50       445 croak "Unable to flush session: $@"
237             if $@;
238              
239 49         256 $self->execute_hook( 'engine.session.after_flush', $session );
240 49         1329 return $session->id;
241             }
242              
243             sub set_cookie_header {
244 88     88 1 511 my ( $self, %params ) = @_;
245             $params{response}->push_header(
246             'Set-Cookie',
247 88         557 $self->cookie( session => $params{session} )->to_header
248             );
249             }
250              
251             sub cookie {
252 88     88 1 333 my ( $self, %params ) = @_;
253 88         254 my $session = $params{session};
254 88 50 33     821 croak "cookie() requires a valid 'session' parameter"
255             unless is_ref($session) && $session->isa("Dancer2::Core::Session");
256              
257 88         2030 my %cookie = (
258             value => $session->id,
259             name => $self->cookie_name,
260             path => $self->cookie_path,
261             secure => $self->is_secure,
262             http_only => $self->is_http_only,
263             );
264              
265 88 100       5907 $cookie{same_site} = $self->cookie_same_site
266             if $self->has_cookie_same_site;
267              
268 88 50       437 $cookie{domain} = $self->cookie_domain
269             if $self->has_cookie_domain;
270              
271 88 100       1972 if ( my $expires = $session->expires ) {
272 14         141 $cookie{expires} = $expires;
273             }
274              
275 88         2579 return Dancer2::Core::Cookie->new(%cookie);
276             }
277              
278             requires '_sessions';
279              
280             sub sessions {
281 0     0 1   my ($self) = @_;
282 0           my $sessions = $self->_sessions;
283              
284 0 0         croak "_sessions() should return an array ref"
285             unless is_arrayref($sessions);
286              
287 0           return $sessions;
288             }
289              
290             1;
291              
292             __END__