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