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