49da39e7d08bce782c3c9e2a34ef90fdad54888e
[catagits/Catalyst-Plugin-Session.git] / lib / Catalyst / Plugin / Session.pm
1 #!/usr/bin/perl
2
3 package Catalyst::Plugin::Session;
4 use base qw/Class::Accessor::Fast/;
5
6 use strict;
7 use warnings;
8
9 use NEXT;
10 use Catalyst::Exception ();
11 use Digest              ();
12 use overload            ();
13
14 our $VERSION = "0.02";
15
16 BEGIN {
17     __PACKAGE__->mk_accessors(qw/_sessionid _session session_delete_reason/);
18 }
19
20 sub setup {
21     my $c = shift;
22
23     $c->NEXT::setup(@_);
24
25     $c->check_session_plugin_requirements;
26     $c->setup_session;
27
28     return $c;
29 }
30
31 sub check_session_plugin_requirements {
32     my $c = shift;
33
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." );
40
41         $c->log->fatal($err);
42         Catalyst::Exception->throw($err);
43     }
44 }
45
46 sub setup_session {
47     my $c = shift;
48
49     my $cfg = ( $c->config->{session} ||= {} );
50
51     %$cfg = (
52         expires        => 7200,
53         verify_address => 1,
54         %$cfg,
55     );
56
57     $c->NEXT::setup_session();
58 }
59
60 sub finalize {
61     my $c = shift;
62
63     if ( my $session_data = $c->_session ) {
64
65         # all sessions are extended at the end of the request
66         my $now = time;
67         @{ $session_data }{qw/__updated __expires/} =
68           ( $now, $c->config->{session}{expires} + $now );
69         $c->store_session_data( $c->sessionid, $session_data );
70     }
71
72     $c->NEXT::finalize(@_);
73 }
74
75 sub prepare_action {
76     my $c = shift;
77
78     $c->_load_session;
79
80     $c->NEXT::prepare_action(@_);
81 }
82
83 sub _load_session {
84     my $c = shift;
85
86     if ( my $sid = $c->sessionid ) {
87                 no warnings 'uninitialized'; # ne __address
88
89         my $session_data = $c->_session || $c->_session( $c->get_session_data($sid) );
90         if ( !$session_data or $session_data->{__expires} < time ) {
91
92             # session expired
93             $c->log->debug("Deleting session $sid (expired)") if $c->debug;
94             $c->delete_session("session expired");
95         }
96         elsif ($c->config->{session}{verify_address}
97             && $session_data->{__address} ne $c->request->address )
98         {
99             $c->log->warn(
100                     "Deleting session $sid due to address mismatch ("
101                   . $session_data->{__address} . " != "
102                   . $c->request->address . ")",
103             );
104             $c->delete_session("address mismatch");
105         }
106         else {
107             $c->log->debug(qq/Restored session "$sid"/) if $c->debug;
108         }
109
110         $c->_expire_ession_keys;
111
112     }
113 }
114
115 sub _expire_ession_keys {
116     my ( $c, $data ) = @_;
117
118     my $now = time;
119
120     my $expiry = ($data || $c->_session || {})->{__expire_keys} || {};
121     foreach my $key (grep { $expiry->{$_} < $now } keys %$expiry ) {
122         delete $c->_session->{$key};
123         delete $expiry->{$key};
124     }
125 }
126
127 sub delete_session {
128     my ( $c, $msg ) = @_;
129
130     # delete the session data
131     my $sid = $c->sessionid;
132     $c->delete_session_data($sid);
133
134     # reset the values in the context object
135     $c->_session(undef);
136     $c->_sessionid(undef);
137     $c->session_delete_reason($msg);
138 }
139
140 sub sessionid {
141         my $c = shift;
142
143         if ( @_ ) {
144                 if ( $c->validate_session_id( my $sid = shift ) ) {
145                         return $c->_sessionid( $sid );
146                 } else {
147                         my $err = "Tried to set invalid session ID '$sid'";
148                         $c->log->error( $err );
149                         Catalyst::Exception->throw( $err );
150                 }
151         }
152
153         return $c->_sessionid;
154 }
155
156 sub validate_session_id {
157         my ( $c, $sid ) = @_;
158
159         $sid =~ /^[a-f\d]+$/i;
160 }
161
162 sub session {
163     my $c = shift;
164
165     $c->_session || do {
166                 my $sid = $c->generate_session_id;
167                 $c->sessionid($sid);
168
169                 $c->log->debug(qq/Created session "$sid"/) if $c->debug;
170
171                 $c->initialize_session_data;
172         };
173 }
174
175 sub session_expire_key {
176     my ( $c, %keys ) = @_;
177
178     my $now = time;
179     @{ $c->session->{__expire_keys} }{keys %keys} = map { $now + $_ } values %keys;
180 }
181
182 sub initialize_session_data {
183     my $c = shift;
184
185     my $now = time;
186
187     return $c->_session({
188         __created => $now,
189         __updated => $now,
190         __expires => $now + $c->config->{session}{expires},
191
192         (
193             $c->config->{session}{verify_address}
194             ? ( __address => $c->request->address )
195             : ()
196         ),
197     });
198 }
199
200 sub generate_session_id {
201     my $c = shift;
202
203     my $digest = $c->_find_digest();
204     $digest->add( $c->session_hash_seed() );
205     return $digest->hexdigest;
206 }
207
208 my $counter;
209
210 sub session_hash_seed {
211     my $c = shift;
212
213     return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
214 }
215
216 my $usable;
217
218 sub _find_digest () {
219     unless ($usable) {
220         foreach my $alg (qw/SHA-1 MD5 SHA-256/) {
221             eval {
222                 my $obj = Digest->new($alg);
223                 $usable = $alg;
224                 return $obj;
225             };
226         }
227         $usable
228           or Catalyst::Exception->throw(
229                 "Could not find a suitable Digest module. Please install "
230               . "Digest::SHA1, Digest::SHA, or Digest::MD5" );
231     }
232
233     return Digest->new($usable);
234 }
235
236 sub dump_these {
237     my $c = shift;
238
239     (
240         $c->NEXT::dump_these(),
241
242         $c->sessionid
243         ? ( [ "Session ID" => $c->sessionid ], [ Session => $c->session ], )
244         : ()
245     );
246 }
247
248 __PACKAGE__;
249
250 __END__
251
252 =pod
253
254 =head1 NAME
255
256 Catalyst::Plugin::Session - Generic Session plugin - ties together server side
257 storage and client side state required to maintain session data.
258
259 =head1 SYNOPSIS
260
261     # To get sessions to "just work", all you need to do is use these plugins:
262
263     use Catalyst qw/
264       Session
265       Session::Store::FastMmap
266       Session::State::Cookie
267       /;
268
269         # you can replace Store::FastMmap with Store::File - both have sensible
270         # default configurations (see their docs for details)
271
272         # more complicated backends are available for other scenarios (DBI storage,
273         # etc)
274
275
276     # after you've loaded the plugins you can save session data
277     # For example, if you are writing a shopping cart, it could be implemented
278     # like this:
279
280     sub add_item : Local {
281         my ( $self, $c ) = @_;
282
283         my $item_id = $c->req->param("item");
284
285         # $c->session is a hash ref, a bit like $c->stash
286         # the difference is that it' preserved across requests
287
288         push @{ $c->session->{items} }, $item_id;
289
290         $c->forward("MyView");
291     }
292
293     sub display_items : Local {
294         my ( $self, $c ) = @_;
295
296         # values in $c->session are restored
297         $c->stash->{items_to_display} =
298           [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
299
300         $c->forward("MyView");
301     }
302
303 =head1 DESCRIPTION
304
305 The Session plugin is the base of two related parts of functionality required
306 for session management in web applications.
307
308 The first part, the State, is getting the browser to repeat back a session key,
309 so that the web application can identify the client and logically string
310 several requests together into a session.
311
312 The second part, the Store, deals with the actual storage of information about
313 the client. This data is stored so that the it may be revived for every request
314 made by the same client.
315
316 This plugin links the two pieces together.
317
318 =head1 RECCOMENDED BACKENDS
319
320 =over 4
321
322 =item Session::State::Cookie
323
324 The only really sane way to do state is using cookies.
325
326 =item Session::Store::File
327
328 A portable backend, based on Cache::File.
329
330 =item Session::Store::FastMmap
331
332 A fast and flexible backend, based on Cache::FastMmap.
333
334 =back
335
336 =head1 METHODS
337
338 =over 4
339
340 =item sessionid
341
342 An accessor for the session ID value.
343
344 =item session
345
346 Returns a hash reference that might contain unserialized values from previous
347 requests in the same session, and whose modified value will be saved for future
348 requests.
349
350 This method will automatically create a new session and session ID if none
351 exists.
352
353 =item session_delete_reason
354
355 This accessor contains a string with the reason a session was deleted. Possible
356 values include:
357
358 =over 4
359
360 =item *
361
362 C<address mismatch>
363
364 =item *
365
366 C<session expired>
367
368 =back
369
370 =item session_expire_key $key, $ttl
371
372 Mark a key to expire at a certain time (only useful when shorter than the
373 expiry time for the whole session).
374
375 For example:
376
377     __PACKAGE__->config->{session}{expires} = 1000000000000; # forever
378
379     # later
380
381     $c->session_expire_key( __user => 3600 );
382
383 Will make the session data survive, but the user will still be logged out after
384 an hour.
385
386 Note that these values are not auto extended.
387
388 =back
389
390 =item INTERNAL METHODS
391
392 =over 4
393
394 =item setup
395
396 This method is extended to also make calls to
397 C<check_session_plugin_requirements> and C<setup_session>.
398
399 =item check_session_plugin_requirements
400
401 This method ensures that a State and a Store plugin are also in use by the
402 application.
403
404 =item setup_session
405
406 This method populates C<< $c->config->{session} >> with the default values
407 listed in L</CONFIGURATION>.
408
409 =item prepare_action
410
411 This methoid is extended, and will restore session data and check it for
412 validity if a session id is defined. It assumes that the State plugin will
413 populate the C<sessionid> key beforehand.
414
415 =item finalize
416
417 This method is extended and will extend the expiry time, as well as persist the
418 session data if a session exists.
419
420 =item delete_session REASON
421
422 This method is used to invalidate a session. It takes an optional parameter
423 which will be saved in C<session_delete_reason> if provided.
424
425 =item initialize_session_data
426
427 This method will initialize the internal structure of the session, and is
428 called by the C<session> method if appropriate.
429
430 =item generate_session_id
431
432 This method will return a string that can be used as a session ID. It is
433 supposed to be a reasonably random string with enough bits to prevent
434 collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
435 MD5 or SHA-256, depending on the availibility of these modules.
436
437 =item session_hash_seed
438
439 This method is actually rather internal to generate_session_id, but should be
440 overridable in case you want to provide more random data.
441
442 Currently it returns a concatenated string which contains:
443
444 =item validate_session_id SID
445
446 Make sure a session ID is of the right format.
447
448 This currently ensures that the session ID string is any amount of case
449 insensitive hexadecimal characters.
450
451 =over 4
452
453 =item *
454
455 A counter
456
457 =item *
458
459 The current time
460
461 =item *
462
463 One value from C<rand>.
464
465 =item *
466
467 The stringified value of a newly allocated hash reference
468
469 =item *
470
471 The stringified value of the Catalyst context object
472
473 =back
474
475 In the hopes that those combined values are entropic enough for most uses. If
476 this is not the case you can replace C<session_hash_seed> with e.g.
477
478     sub session_hash_seed {
479         open my $fh, "<", "/dev/random";
480         read $fh, my $bytes, 20;
481         close $fh;
482         return $bytes;
483     }
484
485 Or even more directly, replace C<generate_session_id>:
486
487     sub generate_session_id {
488         open my $fh, "<", "/dev/random";
489         read $fh, my $bytes, 20;
490         close $fh;
491         return unpack("H*", $bytes);
492     }
493
494 Also have a look at L<Crypt::Random> and the various openssl bindings - these
495 modules provide APIs for cryptographically secure random data.
496
497 =item dump_these
498
499 See L<Catalyst/dump_these> - ammends the session data structure to the list of
500 dumped objects if session ID is defined.
501
502 =back
503
504 =head1 USING SESSIONS DURING PREPARE
505
506 The earliest point in time at which you may use the session data is after
507 L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
508
509 State plugins must set $c->session ID before C<prepare_action>, and during
510 C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
511 the store.
512
513         sub prepare_action {
514                 my $c = shift;
515
516                 # don't touch $c->session yet!
517
518                 $c->NEXT::prepare_action( @_ );
519
520                 $c->session;  # this is OK
521                 $c->sessionid; # this is also OK
522         }
523
524 =head1 CONFIGURATION
525
526     $c->config->{session} = {
527         expires => 1234,
528     };
529
530 All configuation parameters are provided in a hash reference under the
531 C<session> key in the configuration hash.
532
533 =over 4
534
535 =item expires
536
537 The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
538 hours).
539
540 =item verify_address
541
542 When true, C<<$c->request->address>> will be checked at prepare time. If it is
543 not the same as the address that initiated the session, the session is deleted.
544
545 =back
546
547 =head1 SPECIAL KEYS
548
549 The hash reference returned by C<< $c->session >> contains several keys which
550 are automatically set:
551
552 =over 4
553
554 =item __expires
555
556 A timestamp whose value is the last second when the session is still valid. If
557 a session is restored, and __expires is less than the current time, the session
558 is deleted.
559
560 =item __updated
561
562 The last time a session was saved. This is the value of
563 C<< $c->session->{__expires} - $c->config->session->{expires} >>.
564
565 =item __created
566
567 The time when the session was first created.
568
569 =item __address
570
571 The value of C<< $c->request->address >> at the time the session was created.
572 This value is only populated if C<verify_address> is true in the configuration.
573
574 =back
575
576 =head1 CAVEATS
577
578 C<verify_address> could make your site inaccessible to users who are behind
579 load balanced proxies. Some ISPs may give a different IP to each request by the
580 same client due to this type of proxying. If addresses are verified these
581 users' sessions cannot persist.
582
583 To let these users access your site you can either disable address verification
584 as a whole, or provide a checkbox in the login dialog that tells the server
585 that it's OK for the address of the client to change. When the server sees that
586 this box is checked it should delete the C<__address> sepcial key from the
587 session hash when the hash is first created.
588
589 =head1 AUTHORS
590
591 =over 4
592
593 =item Andy Grundman
594
595 =item Christian Hansen
596
597 =item Yuval Kogman, C<nothingmuch@woobling.org> (current maintainer)
598
599 =item Sebastian Riedel
600
601 =back
602
603 And countless other contributers from #catalyst. Thanks guys!
604
605 =head1 COPYRIGHT & LICENSE
606
607         Copyright (c) 2005 the aforementioned authors. All rights
608         reserved. This program is free software; you can redistribute
609         it and/or modify it under the same terms as Perl itself.
610
611 =cut
612
613