remove use of List::Util::first due to leaking
[catagits/Catalyst-Plugin-Session.git] / lib / Catalyst / Plugin / Session.pm
CommitLineData
9e447f9d 1#!/usr/bin/perl
2
3package Catalyst::Plugin::Session;
4use base qw/Class::Accessor::Fast/;
5
6use strict;
7use warnings;
8
9use NEXT;
10use Catalyst::Exception ();
9a9252c2 11use Digest ();
12use overload ();
9e447f9d 13
37160715 14our $VERSION = "0.01";
15
9e447f9d 16BEGIN {
9a9252c2 17 __PACKAGE__->mk_accessors(qw/sessionid session_delete_reason/);
9e447f9d 18}
19
20sub setup {
9a9252c2 21 my $c = shift;
22
23 $c->NEXT::setup(@_);
24
25 $c->check_session_plugin_requirements;
26 $c->setup_session;
27
28 return $c;
9e447f9d 29}
30
31sub check_session_plugin_requirements {
9a9252c2 32 my $c = shift;
9e447f9d 33
9a9252c2 34 unless ( $c->isa("Catalyst::Plugin::Session::State")
35 && $c->isa("Catalyst::Plugin::Session::Store") )
36 {
37 my $err =
38 ( "The Session plugin requires both Session::State "
39 . "and Session::Store plugins to be used as well." );
9e447f9d 40
9a9252c2 41 $c->log->fatal($err);
42 Catalyst::Exception->throw($err);
43 }
9e447f9d 44}
45
46sub setup_session {
9a9252c2 47 my $c = shift;
9e447f9d 48
9a9252c2 49 my $cfg = ( $c->config->{session} ||= {} );
9e447f9d 50
9a9252c2 51 %$cfg = (
52 expires => 7200,
53 verify_address => 1,
54 %$cfg,
55 );
9e447f9d 56
9a9252c2 57 $c->NEXT::setup_session();
9e447f9d 58}
59
60sub finalize {
9a9252c2 61 my $c = shift;
9e447f9d 62
9a9252c2 63 if ( $c->{session} ) {
9e447f9d 64
9a9252c2 65 # all sessions are extended at the end of the request
66 my $now = time;
67 @{ $c->{session} }{qw/__updated __expires/} =
68 ( $now, $c->config->{session}{expires} + $now );
69 $c->store_session_data( $c->sessionid, $c->{session} );
70 }
71
72 $c->NEXT::finalize(@_);
9e447f9d 73}
74
75sub prepare_action {
76 my $c = shift;
77
9a9252c2 78 my $ret = $c->NEXT::prepare_action;
9e447f9d 79
9a9252c2 80 my $sid = $c->sessionid || return;
9e447f9d 81
82 $c->log->debug(qq/Found session "$sid"/) if $c->debug;
83
9a9252c2 84 my $s = $c->{session} ||= $c->get_session_data($sid);
85 if ( !$s or $s->{__expires} < time ) {
86
87 # session expired
88 $c->log->debug("Deleting session $sid (expired)") if $c->debug;
89 $c->delete_session("session expired");
90 return $ret;
91 }
92
93 if ( $c->config->{session}{verify_address}
94 && $c->{session}{__address}
95 && $c->{session}{__address} ne $c->request->address )
96 {
97 $c->log->warn(
98 "Deleting session $sid due to address mismatch ("
99 . $c->{session}{__address} . " != "
100 . $c->request->address . ")",
101 );
102 $c->delete_session("address mismatch");
103 return $ret;
104 }
9e447f9d 105}
106
107sub delete_session {
9a9252c2 108 my ( $c, $msg ) = @_;
9e447f9d 109
9a9252c2 110 # delete the session data
111 my $sid = $c->sessionid;
112 $c->delete_session_data($sid);
9e447f9d 113
9a9252c2 114 # reset the values in the context object
115 $c->{session} = undef;
116 $c->sessionid(undef);
117 $c->session_delete_reason($msg);
9e447f9d 118}
119
120sub session {
9a9252c2 121 my $c = shift;
9e447f9d 122
123 return $c->{session} if $c->{session};
124
9a9252c2 125 my $sid = $c->generate_session_id;
126 $c->sessionid($sid);
9e447f9d 127
9a9252c2 128 $c->log->debug(qq/Created session "$sid"/) if $c->debug;
9e447f9d 129
9a9252c2 130 return $c->initialize_session_data;
9e447f9d 131}
132
133sub initialize_session_data {
9a9252c2 134 my $c = shift;
9e447f9d 135
9a9252c2 136 my $now = time;
9e447f9d 137
9a9252c2 138 return $c->{session} = {
139 __created => $now,
140 __updated => $now,
141 __expires => $now + $c->config->{session}{expires},
9e447f9d 142
9a9252c2 143 (
144 $c->config->{session}{verify_address}
145 ? ( __address => $c->request->address )
146 : ()
147 ),
148 };
9e447f9d 149}
150
9e447f9d 151sub generate_session_id {
152 my $c = shift;
153
154 my $digest = $c->_find_digest();
155 $digest->add( $c->session_hash_seed() );
156 return $digest->hexdigest;
157}
158
159my $counter;
9a9252c2 160
9e447f9d 161sub session_hash_seed {
9a9252c2 162 my $c = shift;
163
164 return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
9e447f9d 165}
166
167my $usable;
9a9252c2 168
9e447f9d 169sub _find_digest () {
9a9252c2 170 unless ($usable) {
7d139eeb 171 foreach my $alg (qw/SHA-1 MD5 SHA-256/) {
172 eval {
173 my $obj = Digest->new($alg);
174 $usable = $alg;
175 return $obj;
176 }
177 }
178 $usable
9a9252c2 179 or Catalyst::Exception->throw(
180 "Could not find a suitable Digest module. Please install "
181 . "Digest::SHA1, Digest::SHA, or Digest::MD5" );
182 }
9e447f9d 183
184 return Digest->new($usable);
185}
186
99b2191e 187sub dump_these {
188 my $c = shift;
189
190 (
191 $c->NEXT::dump_these(),
192
193 $c->sessionid
194 ? ( [ "Session ID" => $c->sessionid ], [ Session => $c->session ], )
195 : ()
196 );
197}
198
9e447f9d 199__PACKAGE__;
200
201__END__
202
203=pod
204
205=head1 NAME
206
207Catalyst::Plugin::Session - Generic Session plugin - ties together server side
208storage and client side tickets required to maintain session data.
209
210=head1 SYNOPSIS
211
212 use Catalyst qw/Session Session::Store::FastMmap Session::State::Cookie/;
213
229a5b53 214 sub add_item : Local {
215 my ( $self, $c ) = @_;
216
217 my $item_id = $c->req->param("item");
218
219 # $c->session is stored across requests, so
220 # other actions will see these values
221
222 push @{ $c->session->{items} }, $item_id;
223
224 $c->forward("MyView");
225 }
226
227 sub display_items : Local {
228 my ( $self, $c ) = @_;
229
230 # values in $c->session are restored
231 $c->stash->{items_to_display} =
232 [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
233
234 $c->forward("MyView");
235 }
236
9e447f9d 237=head1 DESCRIPTION
238
239The Session plugin is the base of two related parts of functionality required
240for session management in web applications.
241
242The first part, the State, is getting the browser to repeat back a session key,
243so that the web application can identify the client and logically string
244several requests together into a session.
245
246The second part, the Store, deals with the actual storage of information about
247the client. This data is stored so that the it may be revived for every request
248made by the same client.
249
250This plugin links the two pieces together.
251
252=head1 METHODS
253
254=over 4
255
256=item sessionid
257
258An accessor for the session ID value.
259
260=item session
261
262Returns a hash reference that might contain unserialized values from previous
263requests in the same session, and whose modified value will be saved for future
264requests.
265
266This method will automatically create a new session and session ID if none
267exists.
268
269=item session_delete_reason
270
271This accessor contains a string with the reason a session was deleted. Possible
272values include:
273
274=over 4
275
276=item *
277
278C<address mismatch>
279
280=item *
281
282C<session expired>
283
284=back
285
286=item setup
287
288This method is extended to also make calls to
289C<check_session_plugin_requirements> and C<setup_session>.
290
291=item check_session_plugin_requirements
292
293This method ensures that a State and a Store plugin are also in use by the
294application.
295
296=item setup_session
297
298This method populates C<< $c->config->{session} >> with the default values
299listed in L</CONFIGURATION>.
300
301=item prepare_action
302
303This methoid is extended, and will restore session data and check it for
304validity if a session id is defined. It assumes that the State plugin will
305populate the C<sessionid> key beforehand.
306
307=item finalize
308
309This method is extended and will extend the expiry time, as well as persist the
310session data if a session exists.
311
312=item delete_session REASON
313
314This method is used to invalidate a session. It takes an optional parameter
315which will be saved in C<session_delete_reason> if provided.
316
317=item initialize_session_data
318
319This method will initialize the internal structure of the session, and is
320called by the C<session> method if appropriate.
321
229a5b53 322=item generate_session_id
323
324This method will return a string that can be used as a session ID. It is
325supposed to be a reasonably random string with enough bits to prevent
326collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
327MD5 or SHA-256, depending on the availibility of these modules.
328
329=item session_hash_seed
330
331This method is actually rather internal to generate_session_id, but should be
332overridable in case you want to provide more random data.
333
334Currently it returns a concatenated string which contains:
335
336=over 4
337
338=item *
339
340A counter
341
342=item *
343
344The current time
345
346=item *
347
348One value from C<rand>.
349
350=item *
351
352The stringified value of a newly allocated hash reference
353
354=item *
355
356The stringified value of the Catalyst context object
357
358=back
359
360In the hopes that those combined values are entropic enough for most uses. If
361this is not the case you can replace C<session_hash_seed> with e.g.
362
363 sub session_hash_seed {
364 open my $fh, "<", "/dev/random";
365 read $fh, my $bytes, 20;
366 close $fh;
367 return $bytes;
368 }
369
370Or even more directly, replace C<generate_session_id>:
371
372 sub generate_session_id {
373 open my $fh, "<", "/dev/random";
374 read $fh, my $bytes, 20;
375 close $fh;
376 return unpack("H*", $bytes);
377 }
378
379Also have a look at L<Crypt::Random> and the various openssl bindings - these
380modules provide APIs for cryptographically secure random data.
381
99b2191e 382=item dump_these
383
384See L<Catalyst/dump_these> - ammends the session data structure to the list of
385dumped objects if session ID is defined.
386
9e447f9d 387=back
388
389=head1 CONFIGURATION
390
229a5b53 391 $c->config->{session} = {
392 expires => 1234,
393 };
9e447f9d 394
395All configuation parameters are provided in a hash reference under the
396C<session> key in the configuration hash.
397
398=over 4
399
400=item expires
401
402The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
403hours).
404
405=item verify_address
406
407When false, C<< $c->request->address >> will be checked at prepare time. If it
408is not the same as the address that initiated the session, the session is
409deleted.
410
411=back
412
413=head1 SPECIAL KEYS
414
415The hash reference returned by C<< $c->session >> contains several keys which
416are automatically set:
417
418=over 4
419
420=item __expires
421
422A timestamp whose value is the last second when the session is still valid. If
423a session is restored, and __expires is less than the current time, the session
424is deleted.
425
426=item __updated
427
428The last time a session was saved. This is the value of
429C<< $c->{session}{__expires} - $c->config->{session}{expires} >>.
430
431=item __created
432
433The time when the session was first created.
434
435=item __address
436
437The value of C<< $c->request->address >> at the time the session was created.
438This value is only populated of C<verify_address> is true in the configuration.
439
440=back
441
c80e9f04 442=head1 CAVEATS
443
444C<verify_address> could make your site inaccessible to users who are behind
445load balanced proxies. Some ISPs may give a different IP to each request by the
446same client due to this type of proxying. If addresses are verified these
447users' sessions cannot persist.
448
449To let these users access your site you can either disable address verification
450as a whole, or provide a checkbox in the login dialog that tells the server
451that it's OK for the address of the client to change. When the server sees that
452this box is checked it should delete the C<__address> sepcial key from the
453session hash when the hash is first created.
454
9e447f9d 455=cut
456
457