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.1.0';
4 128     128   476793 use Moo::Role;
  128         21268  
  128         1229  
5             with 'Dancer2::Core::Role::Engine';
6              
7 128     128   80360 use Carp 'croak';
  128         431  
  128         9107  
8 128     128   74166 use Dancer2::Core::Session;
  128         1154  
  128         6521  
9 128     128   1430 use Dancer2::Core::Types;
  128         368  
  128         1215  
10 128     128   2075975 use Digest::SHA 'sha1';
  128         499283  
  128         23251  
11 128     128   1231 use List::Util 'shuffle';
  128         584  
  128         12984  
12 128     128   70716 use MIME::Base64 'encode_base64url';
  128         108674  
  128         11299  
13 128     128   4756 use Module::Runtime 'require_module';
  128         14374  
  128         1320  
14 128     128   12200 use Ref::Util qw< is_ref is_arrayref is_hashref >;
  128         5278  
  128         284803  
15              
16 20335     20335 0 54827 sub hook_aliases { +{} }
17             sub supported_hooks {
18 34     34 0 673 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 364253 my ($self) = @_;
97              
98 10046         25809 my %args = ( id => $self->generate_id, );
99              
100 10046 100       146569 $args{expires} = $self->cookie_duration
101             if $self->has_cookie_duration;
102              
103 10046         251362 my $session = Dancer2::Core::Session->new(%args);
104              
105 10046         254632 $self->execute_hook( 'engine.session.before_create', $session );
106              
107             # XXX why do we _flush now? Seems unnecessary -- xdg, 2013-03-03
108 10046         88085 eval { $self->_flush( $session->id, $session->data ) };
  10046         201597  
109 10046 50       23777 croak "Unable to create a new session: $@"
110             if $@;
111              
112 10046         29419 $self->execute_hook( 'engine.session.after_create', $session );
113 10046         103859 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 18928 my ($self) = @_;
128              
129 10047 50       23630 if ($CPRNG_AVAIL) {
130             $CPRNG ||= Math::Random::ISAAC::XS->new(
131 10047   66     24302 map { unpack( "N", Crypt::URandom::urandom(4) ) } 1 .. 256 );
  3840         67817  
132              
133             # include $$ to ensure $CPRNG wasn't forked by accident
134 10047         134095 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 121 my ($self, $id) = @_;
161 53         613 return $id =~ m/^[A-Za-z0-9_\-~]+$/;
162             }
163              
164             requires '_retrieve';
165              
166             sub retrieve {
167 54     54 1 792 my ( $self, %params ) = @_;
168 54         130 my $id = $params{id};
169              
170 54         271 $self->execute_hook( 'engine.session.before_retrieve', $id );
171              
172 54         395 my $data;
173             # validate format of session id before attempt to retrieve
174 54         98 my $rc = eval {
175 54 100       207 $self->validate_id($id) && ( $data = $self->_retrieve($id) );
176             };
177 54 100       805 croak "Unable to retrieve session with id '$id'"
178             if ! $rc;
179              
180 51         234 my %args = ( id => $id, );
181              
182 51 50 33     371 $args{data} = $data
183             if $data and is_hashref($data);
184              
185 51 100       279 $args{expires} = $self->cookie_duration
186             if $self->has_cookie_duration;
187              
188 51         1735 my $session = Dancer2::Core::Session->new(%args);
189              
190 51         1504 $self->execute_hook( 'engine.session.after_retrieve', $session );
191 51         505 return $session;
192             }
193              
194             # XXX eventually we could perhaps require '_change_id'?
195              
196             sub change_id {
197 3     3 1 14 my ( $self, %params ) = @_;
198 3         10 my $session = $params{session};
199 3         68 my $old_id = $session->id;
200              
201 3         32 $self->execute_hook( 'engine.session.before_change_id', $old_id );
202              
203 3         31 my $new_id = $self->generate_id;
204 3         101 $session->id( $new_id );
205              
206 3         91 eval { $self->_change_id( $old_id, $new_id ) };
  3         14  
207 3 50       312 croak "Unable to change session id for session with id $old_id: $@"
208             if $@;
209              
210 3         14 $self->execute_hook( 'engine.session.after_change_id', $new_id );
211             }
212              
213             requires '_destroy';
214              
215             sub destroy {
216 17     17 1 205 my ( $self, %params ) = @_;
217 17         47 my $id = $params{id};
218 17         81 $self->execute_hook( 'engine.session.before_destroy', $id );
219              
220 17         97 eval { $self->_destroy($id) };
  17         89  
221 17 50       107 croak "Unable to destroy session with id '$id': $@"
222             if $@;
223              
224 17         77 $self->execute_hook( 'engine.session.after_destroy', $id );
225 17         141 return $id;
226             }
227              
228             requires '_flush';
229              
230             sub flush {
231 49     49 1 1139 my ( $self, %params ) = @_;
232 49         129 my $session = $params{session};
233 49         251 $self->execute_hook( 'engine.session.before_flush', $session );
234              
235 49         377 eval { $self->_flush( $session->id, $session->data ) };
  49         874  
236 49 50       399 croak "Unable to flush session: $@"
237             if $@;
238              
239 49         250 $self->execute_hook( 'engine.session.after_flush', $session );
240 49         1111 return $session->id;
241             }
242              
243             sub set_cookie_header {
244 88     88 1 513 my ( $self, %params ) = @_;
245             $params{response}->push_header(
246             'Set-Cookie',
247 88         508 $self->cookie( session => $params{session} )->to_header
248             );
249             }
250              
251             sub cookie {
252 88     88 1 1585 my ( $self, %params ) = @_;
253 88         230 my $session = $params{session};
254 88 50 33     868 croak "cookie() requires a valid 'session' parameter"
255             unless is_ref($session) && $session->isa("Dancer2::Core::Session");
256              
257 88         1638 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       4908 $cookie{same_site} = $self->cookie_same_site
266             if $self->has_cookie_same_site;
267              
268 88 50       345 $cookie{domain} = $self->cookie_domain
269             if $self->has_cookie_domain;
270              
271 88 100       1577 if ( my $expires = $session->expires ) {
272 14         124 $cookie{expires} = $expires;
273             }
274              
275 88         2349 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__