version bump C::P::Session
[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 use Object::Signature   ();
14
15 our $VERSION = "0.04";
16
17 BEGIN {
18     __PACKAGE__->mk_accessors(
19         qw/
20           _sessionid
21           _session
22           _session_expires
23           _session_data_sig
24           _session_delete_reason
25           _flash
26           _flash_stale_keys
27           /
28     );
29 }
30
31 sub setup {
32     my $c = shift;
33
34     $c->NEXT::setup(@_);
35
36     $c->check_session_plugin_requirements;
37     $c->setup_session;
38
39     return $c;
40 }
41
42 sub check_session_plugin_requirements {
43     my $c = shift;
44
45     unless ( $c->isa("Catalyst::Plugin::Session::State")
46         && $c->isa("Catalyst::Plugin::Session::Store") )
47     {
48         my $err =
49           (     "The Session plugin requires both Session::State "
50               . "and Session::Store plugins to be used as well." );
51
52         $c->log->fatal($err);
53         Catalyst::Exception->throw($err);
54     }
55 }
56
57 sub setup_session {
58     my $c = shift;
59
60     my $cfg = ( $c->config->{session} ||= {} );
61
62     %$cfg = (
63         expires        => 7200,
64         verify_address => 1,
65         %$cfg,
66     );
67
68     $c->NEXT::setup_session();
69 }
70
71 sub prepare_action {
72     my $c = shift;
73
74     if (    $c->config->{session}{flash_to_stash}
75         and $c->_sessionid
76         and my $flash_data = $c->flash )
77     {
78         @{ $c->stash }{ keys %$flash_data } = values %$flash_data;
79     }
80
81     $c->NEXT::prepare_action(@_);
82 }
83
84 sub finalize {
85     my $c = shift;
86
87     $c->_save_session;
88     $c->_save_flash;
89
90     $c->NEXT::finalize(@_);
91 }
92
93 sub _save_session {
94     my $c = shift;
95
96     if ( my $sid = $c->_sessionid ) {
97         if ( my $session_data = $c->_session ) {
98
99             # all sessions are extended at the end of the request
100             my $now = time;
101
102             # the ref is a workaround for FastMmap:
103             # FastMmap can't store values which aren't refs
104             # this yields errors and other great suckage
105             $c->store_session_data( "expires:$sid" =>
106                   ( { expires => $c->config->{session}{expires} + $now } ) );
107
108             no warnings 'uninitialized';
109             if ( Object::Signature::signature($session_data) ne
110                 $c->_session_data_sig )
111             {
112                 $session_data->{__updated} = $now;
113                 $c->store_session_data( "session:$sid" => $session_data );
114             }
115         }
116     }
117 }
118
119 sub _save_flash {
120     my $c = shift;
121
122     if ( my $sid = $c->_sessionid ) {
123         my $flash_data = $c->_flash || {};
124
125         delete @{$flash_data}{ @{ $c->_flash_stale_keys || [] } };
126
127         if (%$flash_data) {    # damn 'my' declarations
128             $c->store_session_data( "flash:$sid", $flash_data );
129         }
130         else {
131             $c->delete_session_data("flash:$sid");
132         }
133     }
134 }
135
136 sub _load_session {
137     my $c = shift;
138
139     if ( my $sid = $c->_sessionid ) {
140         no warnings 'uninitialized';    # ne __address
141
142         # see above for explanation  of the workaround for FastMmap problem
143         my $session_expires =
144           ( $c->get_session_data("expires:$sid") || { expires => 0 } )
145           ->{expires};
146
147         if ( $session_expires < time ) {
148
149             # session expired
150             $c->log->debug("Deleting session $sid (expired)") if $c->debug;
151             $c->delete_session("session expired");
152             return;
153         }
154
155         my $session_data = $c->get_session_data("session:$sid");
156         $c->_session($session_data);
157
158         if (   $c->config->{session}{verify_address}
159             && $session_data->{__address} ne $c->request->address )
160         {
161             $c->log->warn(
162                     "Deleting session $sid due to address mismatch ("
163                   . $session_data->{__address} . " != "
164                   . $c->request->address . ")",
165             );
166             $c->delete_session("address mismatch");
167             return;
168         }
169
170         $c->log->debug(qq/Restored session "$sid"/) if $c->debug;
171         $c->_session_data_sig( Object::Signature::signature($session_data) );
172         $c->_expire_session_keys;
173
174         return $session_data;
175     }
176
177     return;
178 }
179
180 sub _load_flash {
181     my $c = shift;
182
183     if ( my $sid = $c->_sessionid ) {
184         if ( my $flash_data = $c->_flash
185             || $c->_flash( $c->get_session_data("flash:$sid") ) )
186         {
187             $c->_flash_stale_keys( [ keys %$flash_data ] );
188             return $flash_data;
189         }
190     }
191
192     return undef;
193 }
194
195 sub _expire_session_keys {
196     my ( $c, $data ) = @_;
197
198     my $now = time;
199
200     my $expiry = ( $data || $c->_session || {} )->{__expire_keys} || {};
201     foreach my $key ( grep { $expiry->{$_} < $now } keys %$expiry ) {
202         delete $c->_session->{$key};
203         delete $expiry->{$key};
204     }
205 }
206
207 sub delete_session {
208     my ( $c, $msg ) = @_;
209
210     # delete the session data
211     my $sid = $c->_sessionid || return;
212     $c->delete_session_data("${_}:${sid}") for qw/session expires flash/;
213
214     # reset the values in the context object
215     $c->_session(undef);
216     $c->_sessionid(undef);
217     $c->_session_delete_reason($msg);
218 }
219
220 sub session_delete_reason {
221     my $c = shift;
222
223     $c->_load_session
224       if ( $c->_sessionid && !$c->_session );    # must verify session data
225
226     $c->_session_delete_reason(@_);
227 }
228
229 sub sessionid {
230     my $c = shift;
231
232     if (@_) {
233         if ( $c->validate_session_id( my $sid = shift ) ) {
234             $c->_sessionid($sid);
235             return unless defined wantarray;
236         }
237         else {
238             my $err = "Tried to set invalid session ID '$sid'";
239             $c->log->error($err);
240             Catalyst::Exception->throw($err);
241         }
242     }
243
244     $c->_load_session
245       if ( $c->_sessionid && !$c->_session );    # must verify session data
246
247     return $c->_sessionid;
248 }
249
250 sub validate_session_id {
251     my ( $c, $sid ) = @_;
252
253     $sid and $sid =~ /^[a-f\d]+$/i;
254 }
255
256 sub session {
257     my $c = shift;
258
259     $c->_session || $c->_load_session || do {
260         $c->create_session_id;
261
262         $c->initialize_session_data;
263     };
264 }
265
266 sub flash {
267     my $c = shift;
268     $c->_flash || $c->_load_flash || do {
269         $c->create_session_id;
270         $c->_flash( {} );
271       }
272 }
273
274 sub session_expire_key {
275     my ( $c, %keys ) = @_;
276
277     my $now = time;
278     @{ $c->session->{__expire_keys} }{ keys %keys } =
279       map { $now + $_ } values %keys;
280 }
281
282 sub initialize_session_data {
283     my $c = shift;
284
285     my $now = time;
286
287     return $c->_session(
288         {
289             __created => $now,
290             __updated => $now,
291
292             (
293                 $c->config->{session}{verify_address}
294                 ? ( __address => $c->request->address )
295                 : ()
296             ),
297         }
298     );
299 }
300
301 sub generate_session_id {
302     my $c = shift;
303
304     my $digest = $c->_find_digest();
305     $digest->add( $c->session_hash_seed() );
306     return $digest->hexdigest;
307 }
308
309 sub create_session_id {
310     my $c = shift;
311
312     if ( !$c->_sessionid ) {
313         my $sid = $c->generate_session_id;
314
315         $c->log->debug(qq/Created session "$sid"/) if $c->debug;
316
317         $c->sessionid($sid);
318     }
319 }
320
321 my $counter;
322
323 sub session_hash_seed {
324     my $c = shift;
325
326     return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
327 }
328
329 my $usable;
330
331 sub _find_digest () {
332     unless ($usable) {
333         foreach my $alg (qw/SHA-1 SHA-256 MD5/) {
334             if ( eval { Digest->new($alg) } ) {
335                 $usable = $alg;
336                 last;
337             }
338         }
339         Catalyst::Exception->throw(
340                 "Could not find a suitable Digest module. Please install "
341               . "Digest::SHA1, Digest::SHA, or Digest::MD5" )
342           unless $usable;
343     }
344
345     return Digest->new($usable);
346 }
347
348 sub dump_these {
349     my $c = shift;
350
351     (
352         $c->NEXT::dump_these(),
353
354         $c->sessionid
355         ? ( [ "Session ID" => $c->sessionid ], [ Session => $c->session ], )
356         : ()
357     );
358 }
359
360 __PACKAGE__;
361
362 __END__
363
364 =pod
365
366 =head1 NAME
367
368 Catalyst::Plugin::Session - Generic Session plugin - ties together server side
369 storage and client side state required to maintain session data.
370
371 =head1 SYNOPSIS
372
373     # To get sessions to "just work", all you need to do is use these plugins:
374
375     use Catalyst qw/
376       Session
377       Session::Store::FastMmap
378       Session::State::Cookie
379       /;
380
381         # you can replace Store::FastMmap with Store::File - both have sensible
382         # default configurations (see their docs for details)
383
384         # more complicated backends are available for other scenarios (DBI storage,
385         # etc)
386
387
388     # after you've loaded the plugins you can save session data
389     # For example, if you are writing a shopping cart, it could be implemented
390     # like this:
391
392     sub add_item : Local {
393         my ( $self, $c ) = @_;
394
395         my $item_id = $c->req->param("item");
396
397         # $c->session is a hash ref, a bit like $c->stash
398         # the difference is that it' preserved across requests
399
400         push @{ $c->session->{items} }, $item_id;
401
402         $c->forward("MyView");
403     }
404
405     sub display_items : Local {
406         my ( $self, $c ) = @_;
407
408         # values in $c->session are restored
409         $c->stash->{items_to_display} =
410           [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
411
412         $c->forward("MyView");
413     }
414
415 =head1 DESCRIPTION
416
417 The Session plugin is the base of two related parts of functionality required
418 for session management in web applications.
419
420 The first part, the State, is getting the browser to repeat back a session key,
421 so that the web application can identify the client and logically string
422 several requests together into a session.
423
424 The second part, the Store, deals with the actual storage of information about
425 the client. This data is stored so that the it may be revived for every request
426 made by the same client.
427
428 This plugin links the two pieces together.
429
430 =head1 RECCOMENDED BACKENDS
431
432 =over 4
433
434 =item Session::State::Cookie
435
436 The only really sane way to do state is using cookies.
437
438 =item Session::Store::File
439
440 A portable backend, based on Cache::File.
441
442 =item Session::Store::FastMmap
443
444 A fast and flexible backend, based on Cache::FastMmap.
445
446 =back
447
448 =head1 METHODS
449
450 =over 4
451
452 =item sessionid
453
454 An accessor for the session ID value.
455
456 =item session
457
458 Returns a hash reference that might contain unserialized values from previous
459 requests in the same session, and whose modified value will be saved for future
460 requests.
461
462 This method will automatically create a new session and session ID if none
463 exists.
464
465 =item flash
466
467 This is like Ruby on Rails' flash data structure. Think of it as a stash that
468 lasts a single redirect, not only a forward.
469
470     sub moose : Local {
471         my ( $self, $c ) = @_;
472
473         $c->flash->{beans} = 10;
474         $c->response->redirect( $c->uri_for("foo") );
475     }
476
477     sub foo : Local {
478         my ( $self, $c ) = @_;
479
480         my $value = $c->flash->{beans};
481
482         # ...
483
484         $c->response->redirect( $c->uri_for("bar") );
485     }
486
487     sub bar : Local {
488         my ( $self, $c ) = @_;
489
490         if ( exists $c->flash->{beans} ) { # false
491         
492         }
493     }
494
495 =item session_delete_reason
496
497 This accessor contains a string with the reason a session was deleted. Possible
498 values include:
499
500 =over 4
501
502 =item *
503
504 C<address mismatch>
505
506 =item *
507
508 C<session expired>
509
510 =back
511
512 =item session_expire_key $key, $ttl
513
514 Mark a key to expire at a certain time (only useful when shorter than the
515 expiry time for the whole session).
516
517 For example:
518
519     __PACKAGE__->config->{session}{expires} = 1000000000000; # forever
520
521     # later
522
523     $c->session_expire_key( __user => 3600 );
524
525 Will make the session data survive, but the user will still be logged out after
526 an hour.
527
528 Note that these values are not auto extended.
529
530 =back
531
532 =head1 INTERNAL METHODS
533
534 =over 4
535
536 =item setup
537
538 This method is extended to also make calls to
539 C<check_session_plugin_requirements> and C<setup_session>.
540
541 =item check_session_plugin_requirements
542
543 This method ensures that a State and a Store plugin are also in use by the
544 application.
545
546 =item setup_session
547
548 This method populates C<< $c->config->{session} >> with the default values
549 listed in L</CONFIGURATION>.
550
551 =item prepare_action
552
553 This methoid is extended.
554
555 It's only effect is if the (off by default) C<flash_to_stash> configuration
556 parameter is on - then it will copy the contents of the flash to the stash at
557 prepare time.
558
559 =item finalize
560
561 This method is extended and will extend the expiry time, as well as persist the
562 session data if a session exists.
563
564 =item delete_session REASON
565
566 This method is used to invalidate a session. It takes an optional parameter
567 which will be saved in C<session_delete_reason> if provided.
568
569 =item initialize_session_data
570
571 This method will initialize the internal structure of the session, and is
572 called by the C<session> method if appropriate.
573
574 =item create_session_id
575
576 Creates a new session id using C<generate_session_id> if there is no session ID
577 yet.
578
579 =item generate_session_id
580
581 This method will return a string that can be used as a session ID. It is
582 supposed to be a reasonably random string with enough bits to prevent
583 collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
584 MD5 or SHA-256, depending on the availibility of these modules.
585
586 =item session_hash_seed
587
588 This method is actually rather internal to generate_session_id, but should be
589 overridable in case you want to provide more random data.
590
591 Currently it returns a concatenated string which contains:
592
593 =item validate_session_id SID
594
595 Make sure a session ID is of the right format.
596
597 This currently ensures that the session ID string is any amount of case
598 insensitive hexadecimal characters.
599
600 =over 4
601
602 =item *
603
604 A counter
605
606 =item *
607
608 The current time
609
610 =item *
611
612 One value from C<rand>.
613
614 =item *
615
616 The stringified value of a newly allocated hash reference
617
618 =item *
619
620 The stringified value of the Catalyst context object
621
622 =back
623
624 In the hopes that those combined values are entropic enough for most uses. If
625 this is not the case you can replace C<session_hash_seed> with e.g.
626
627     sub session_hash_seed {
628         open my $fh, "<", "/dev/random";
629         read $fh, my $bytes, 20;
630         close $fh;
631         return $bytes;
632     }
633
634 Or even more directly, replace C<generate_session_id>:
635
636     sub generate_session_id {
637         open my $fh, "<", "/dev/random";
638         read $fh, my $bytes, 20;
639         close $fh;
640         return unpack("H*", $bytes);
641     }
642
643 Also have a look at L<Crypt::Random> and the various openssl bindings - these
644 modules provide APIs for cryptographically secure random data.
645
646 =item dump_these
647
648 See L<Catalyst/dump_these> - ammends the session data structure to the list of
649 dumped objects if session ID is defined.
650
651 =back
652
653 =head1 USING SESSIONS DURING PREPARE
654
655 The earliest point in time at which you may use the session data is after
656 L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
657
658 State plugins must set $c->session ID before C<prepare_action>, and during
659 C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
660 the store.
661
662         sub prepare_action {
663                 my $c = shift;
664
665                 # don't touch $c->session yet!
666
667                 $c->NEXT::prepare_action( @_ );
668
669                 $c->session;  # this is OK
670                 $c->sessionid; # this is also OK
671         }
672
673 =head1 CONFIGURATION
674
675     $c->config->{session} = {
676         expires => 1234,
677     };
678
679 All configuation parameters are provided in a hash reference under the
680 C<session> key in the configuration hash.
681
682 =over 4
683
684 =item expires
685
686 The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
687 hours).
688
689 =item verify_address
690
691 When true, C<<$c->request->address>> will be checked at prepare time. If it is
692 not the same as the address that initiated the session, the session is deleted.
693
694 =item flash_to_stash
695
696 This option makes it easier to have actions behave the same whether they were
697 forwarded to or redirected to. On prepare time it copies the contents of
698 C<flash> (if any) to the stash.
699
700 =back
701
702 =head1 SPECIAL KEYS
703
704 The hash reference returned by C<< $c->session >> contains several keys which
705 are automatically set:
706
707 =over 4
708
709 =item __expires
710
711 This key no longer exists. This data is now saved elsewhere.
712
713 =item __updated
714
715 The last time a session was saved to the store.
716
717 =item __created
718
719 The time when the session was first created.
720
721 =item __address
722
723 The value of C<< $c->request->address >> at the time the session was created.
724 This value is only populated if C<verify_address> is true in the configuration.
725
726 =back
727
728 =head1 CAVEATS
729
730 =head2 Round the Robin Proxies
731
732 C<verify_address> could make your site inaccessible to users who are behind
733 load balanced proxies. Some ISPs may give a different IP to each request by the
734 same client due to this type of proxying. If addresses are verified these
735 users' sessions cannot persist.
736
737 To let these users access your site you can either disable address verification
738 as a whole, or provide a checkbox in the login dialog that tells the server
739 that it's OK for the address of the client to change. When the server sees that
740 this box is checked it should delete the C<__address> sepcial key from the
741 session hash when the hash is first created.
742
743 =head2 Race Conditions
744
745 In this day and age where cleaning detergents and dutch football (not the
746 american kind) teams roam the plains in great numbers, requests may happen
747 simultaneously. This means that there is some risk of session data being
748 overwritten, like this:
749
750 =over 4
751
752 =item 1.
753
754 request a starts, request b starts, with the same session id
755
756 =item 2.
757
758 session data is loaded in request a
759
760 =item 3.
761
762 session data is loaded in request b
763
764 =item 4.
765
766 session data is changed in request a
767
768 =item 5.
769
770 request a finishes, session data is updated and written to store
771
772 =item 6.
773
774 request b finishes, session data is updated and written to store, overwriting
775 changes by request a
776
777 =back
778
779 If this is a concern in your application, a soon to be developed locking
780 solution is the only safe way to go. This will have a bigger overhead.
781
782 For applications where any given user is only making one request at a time this
783 plugin should be safe enough.
784
785 =head1 AUTHORS
786
787 =over 4
788
789 =item Andy Grundman
790
791 =item Christian Hansen
792
793 =item Yuval Kogman, C<nothingmuch@woobling.org> (current maintainer)
794
795 =item Sebastian Riedel
796
797 =back
798
799 And countless other contributers from #catalyst. Thanks guys!
800
801 =head1 COPYRIGHT & LICENSE
802
803         Copyright (c) 2005 the aforementioned authors. All rights
804         reserved. This program is free software; you can redistribute
805         it and/or modify it under the same terms as Perl itself.
806
807 =cut
808
809