version bump C::P::Session
[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 ();
d44bc687 13use Object::Signature ();
9e447f9d 14
d0e0cfb8 15our $VERSION = "0.04";
37160715 16
9e447f9d 17BEGIN {
4207ce8d 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 );
9e447f9d 29}
30
31sub setup {
9a9252c2 32 my $c = shift;
33
34 $c->NEXT::setup(@_);
35
36 $c->check_session_plugin_requirements;
37 $c->setup_session;
38
39 return $c;
9e447f9d 40}
41
42sub check_session_plugin_requirements {
9a9252c2 43 my $c = shift;
9e447f9d 44
9a9252c2 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." );
9e447f9d 51
9a9252c2 52 $c->log->fatal($err);
53 Catalyst::Exception->throw($err);
54 }
9e447f9d 55}
56
57sub setup_session {
9a9252c2 58 my $c = shift;
9e447f9d 59
9a9252c2 60 my $cfg = ( $c->config->{session} ||= {} );
9e447f9d 61
9a9252c2 62 %$cfg = (
63 expires => 7200,
64 verify_address => 1,
65 %$cfg,
66 );
9e447f9d 67
9a9252c2 68 $c->NEXT::setup_session();
9e447f9d 69}
70
19c130c2 71sub prepare_action {
72 my $c = shift;
73
4207ce8d 74 if ( $c->config->{session}{flash_to_stash}
75 and $c->_sessionid
76 and my $flash_data = $c->flash )
77 {
19c130c2 78 @{ $c->stash }{ keys %$flash_data } = values %$flash_data;
79 }
80
81 $c->NEXT::prepare_action(@_);
82}
83
9e447f9d 84sub finalize {
9a9252c2 85 my $c = shift;
9e447f9d 86
9b0fa2a6 87 $c->_save_session;
88 $c->_save_flash;
89
90 $c->NEXT::finalize(@_);
91}
92
93sub _save_session {
94 my $c = shift;
4207ce8d 95
ea972e9a 96 if ( my $sid = $c->_sessionid ) {
97 if ( my $session_data = $c->_session ) {
9e447f9d 98
ea972e9a 99 # all sessions are extended at the end of the request
100 my $now = time;
7a02371f 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 } ) );
d44bc687 107
108 no warnings 'uninitialized';
4207ce8d 109 if ( Object::Signature::signature($session_data) ne
110 $c->_session_data_sig )
111 {
d44bc687 112 $session_data->{__updated} = $now;
113 $c->store_session_data( "session:$sid" => $session_data );
114 }
ea972e9a 115 }
9a9252c2 116 }
9b0fa2a6 117}
9a9252c2 118
9b0fa2a6 119sub _save_flash {
120 my $c = shift;
121
ea972e9a 122 if ( my $sid = $c->_sessionid ) {
4207ce8d 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");
ea972e9a 132 }
9b0fa2a6 133 }
9e447f9d 134}
135
b7acf64e 136sub _load_session {
137 my $c = shift;
138
29d15411 139 if ( my $sid = $c->_sessionid ) {
4207ce8d 140 no warnings 'uninitialized'; # ne __address
141
7a02371f 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};
0974ac06 146
4207ce8d 147 if ( $session_expires < time ) {
3f182468 148
149 # session expired
150 $c->log->debug("Deleting session $sid (expired)") if $c->debug;
151 $c->delete_session("session expired");
4207ce8d 152 return;
3f182468 153 }
4207ce8d 154
155 my $session_data = $c->get_session_data("session:$sid");
156 $c->_session($session_data);
157
158 if ( $c->config->{session}{verify_address}
0974ac06 159 && $session_data->{__address} ne $c->request->address )
3f182468 160 {
161 $c->log->warn(
162 "Deleting session $sid due to address mismatch ("
0974ac06 163 . $session_data->{__address} . " != "
3f182468 164 . $c->request->address . ")",
165 );
166 $c->delete_session("address mismatch");
4207ce8d 167 return;
3f182468 168 }
4207ce8d 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;
b7acf64e 173
29d15411 174 return $session_data;
9a9252c2 175 }
29d15411 176
4207ce8d 177 return;
b7acf64e 178}
9a9252c2 179
9b0fa2a6 180sub _load_flash {
181 my $c = shift;
182
183 if ( my $sid = $c->_sessionid ) {
4207ce8d 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 ] );
9b0fa2a6 188 return $flash_data;
189 }
190 }
191
192 return undef;
193}
194
d44bc687 195sub _expire_session_keys {
b7acf64e 196 my ( $c, $data ) = @_;
197
198 my $now = time;
199
4207ce8d 200 my $expiry = ( $data || $c->_session || {} )->{__expire_keys} || {};
201 foreach my $key ( grep { $expiry->{$_} < $now } keys %$expiry ) {
b7acf64e 202 delete $c->_session->{$key};
203 delete $expiry->{$key};
204 }
9e447f9d 205}
206
207sub delete_session {
9a9252c2 208 my ( $c, $msg ) = @_;
9e447f9d 209
9a9252c2 210 # delete the session data
29d15411 211 my $sid = $c->_sessionid || return;
4207ce8d 212 $c->delete_session_data("${_}:${sid}") for qw/session expires flash/;
9e447f9d 213
9a9252c2 214 # reset the values in the context object
0974ac06 215 $c->_session(undef);
216 $c->_sessionid(undef);
29d15411 217 $c->_session_delete_reason($msg);
218}
219
220sub session_delete_reason {
221 my $c = shift;
222
4207ce8d 223 $c->_load_session
224 if ( $c->_sessionid && !$c->_session ); # must verify session data
29d15411 225
4207ce8d 226 $c->_session_delete_reason(@_);
9e447f9d 227}
228
0974ac06 229sub sessionid {
4207ce8d 230 my $c = shift;
231
232 if (@_) {
233 if ( $c->validate_session_id( my $sid = shift ) ) {
234 $c->_sessionid($sid);
29d15411 235 return unless defined wantarray;
4207ce8d 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;
0974ac06 248}
249
250sub validate_session_id {
4207ce8d 251 my ( $c, $sid ) = @_;
0974ac06 252
4207ce8d 253 $sid and $sid =~ /^[a-f\d]+$/i;
0974ac06 254}
255
9e447f9d 256sub session {
9a9252c2 257 my $c = shift;
9e447f9d 258
29d15411 259 $c->_session || $c->_load_session || do {
78476ce0 260 $c->create_session_id;
9e447f9d 261
29d15411 262 $c->initialize_session_data;
4207ce8d 263 };
9e447f9d 264}
265
873f7011 266sub flash {
267 my $c = shift;
78476ce0 268 $c->_flash || $c->_load_flash || do {
269 $c->create_session_id;
270 $c->_flash( {} );
4207ce8d 271 }
873f7011 272}
273
b7acf64e 274sub session_expire_key {
275 my ( $c, %keys ) = @_;
276
277 my $now = time;
4207ce8d 278 @{ $c->session->{__expire_keys} }{ keys %keys } =
279 map { $now + $_ } values %keys;
b7acf64e 280}
281
9e447f9d 282sub initialize_session_data {
9a9252c2 283 my $c = shift;
9e447f9d 284
9a9252c2 285 my $now = time;
9e447f9d 286
4207ce8d 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 );
9e447f9d 299}
300
9e447f9d 301sub 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
78476ce0 309sub 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
9e447f9d 321my $counter;
9a9252c2 322
9e447f9d 323sub session_hash_seed {
9a9252c2 324 my $c = shift;
325
326 return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
9e447f9d 327}
328
329my $usable;
9a9252c2 330
9e447f9d 331sub _find_digest () {
9a9252c2 332 unless ($usable) {
d44bc687 333 foreach my $alg (qw/SHA-1 SHA-256 MD5/) {
4207ce8d 334 if ( eval { Digest->new($alg) } ) {
5faaa4b0 335 $usable = $alg;
336 last;
337 }
7d139eeb 338 }
4207ce8d 339 Catalyst::Exception->throw(
9a9252c2 340 "Could not find a suitable Digest module. Please install "
4207ce8d 341 . "Digest::SHA1, Digest::SHA, or Digest::MD5" )
342 unless $usable;
9a9252c2 343 }
9e447f9d 344
345 return Digest->new($usable);
346}
347
99b2191e 348sub 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
9e447f9d 360__PACKAGE__;
361
362__END__
363
364=pod
365
366=head1 NAME
367
368Catalyst::Plugin::Session - Generic Session plugin - ties together server side
fb1a4ac3 369storage and client side state required to maintain session data.
9e447f9d 370
371=head1 SYNOPSIS
372
8f0b4c16 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:
9e447f9d 391
229a5b53 392 sub add_item : Local {
393 my ( $self, $c ) = @_;
394
395 my $item_id = $c->req->param("item");
396
8f0b4c16 397 # $c->session is a hash ref, a bit like $c->stash
398 # the difference is that it' preserved across requests
229a5b53 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} =
8f0b4c16 410 [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
229a5b53 411
412 $c->forward("MyView");
413 }
414
9e447f9d 415=head1 DESCRIPTION
416
417The Session plugin is the base of two related parts of functionality required
418for session management in web applications.
419
420The first part, the State, is getting the browser to repeat back a session key,
421so that the web application can identify the client and logically string
422several requests together into a session.
423
424The second part, the Store, deals with the actual storage of information about
425the client. This data is stored so that the it may be revived for every request
426made by the same client.
427
428This plugin links the two pieces together.
429
8f0b4c16 430=head1 RECCOMENDED BACKENDS
431
432=over 4
433
434=item Session::State::Cookie
435
436The only really sane way to do state is using cookies.
437
438=item Session::Store::File
439
440A portable backend, based on Cache::File.
441
442=item Session::Store::FastMmap
443
444A fast and flexible backend, based on Cache::FastMmap.
445
446=back
447
9e447f9d 448=head1 METHODS
449
450=over 4
451
452=item sessionid
453
454An accessor for the session ID value.
455
456=item session
457
458Returns a hash reference that might contain unserialized values from previous
459requests in the same session, and whose modified value will be saved for future
460requests.
461
462This method will automatically create a new session and session ID if none
463exists.
464
07e714d2 465=item flash
466
467This is like Ruby on Rails' flash data structure. Think of it as a stash that
468lasts 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
9e447f9d 495=item session_delete_reason
496
497This accessor contains a string with the reason a session was deleted. Possible
498values include:
499
500=over 4
501
502=item *
503
504C<address mismatch>
505
506=item *
507
508C<session expired>
509
510=back
511
b7acf64e 512=item session_expire_key $key, $ttl
513
514Mark a key to expire at a certain time (only useful when shorter than the
515expiry time for the whole session).
516
517For example:
518
519 __PACKAGE__->config->{session}{expires} = 1000000000000; # forever
520
521 # later
522
523 $c->session_expire_key( __user => 3600 );
524
525Will make the session data survive, but the user will still be logged out after
526an hour.
527
528Note that these values are not auto extended.
529
8f0b4c16 530=back
531
10c72079 532=head1 INTERNAL METHODS
8f0b4c16 533
534=over 4
535
9e447f9d 536=item setup
537
538This method is extended to also make calls to
539C<check_session_plugin_requirements> and C<setup_session>.
540
541=item check_session_plugin_requirements
542
543This method ensures that a State and a Store plugin are also in use by the
544application.
545
546=item setup_session
547
548This method populates C<< $c->config->{session} >> with the default values
549listed in L</CONFIGURATION>.
550
551=item prepare_action
552
68fd02ae 553This methoid is extended.
554
555It's only effect is if the (off by default) C<flash_to_stash> configuration
556parameter is on - then it will copy the contents of the flash to the stash at
557prepare time.
9e447f9d 558
559=item finalize
560
561This method is extended and will extend the expiry time, as well as persist the
562session data if a session exists.
563
564=item delete_session REASON
565
566This method is used to invalidate a session. It takes an optional parameter
567which will be saved in C<session_delete_reason> if provided.
568
569=item initialize_session_data
570
571This method will initialize the internal structure of the session, and is
572called by the C<session> method if appropriate.
573
68fd02ae 574=item create_session_id
575
576Creates a new session id using C<generate_session_id> if there is no session ID
577yet.
578
229a5b53 579=item generate_session_id
580
581This method will return a string that can be used as a session ID. It is
582supposed to be a reasonably random string with enough bits to prevent
583collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
584MD5 or SHA-256, depending on the availibility of these modules.
585
586=item session_hash_seed
587
588This method is actually rather internal to generate_session_id, but should be
589overridable in case you want to provide more random data.
590
591Currently it returns a concatenated string which contains:
592
0974ac06 593=item validate_session_id SID
594
595Make sure a session ID is of the right format.
596
597This currently ensures that the session ID string is any amount of case
598insensitive hexadecimal characters.
599
229a5b53 600=over 4
601
602=item *
603
604A counter
605
606=item *
607
608The current time
609
610=item *
611
612One value from C<rand>.
613
614=item *
615
616The stringified value of a newly allocated hash reference
617
618=item *
619
620The stringified value of the Catalyst context object
621
622=back
623
624In the hopes that those combined values are entropic enough for most uses. If
625this 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
634Or 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
643Also have a look at L<Crypt::Random> and the various openssl bindings - these
644modules provide APIs for cryptographically secure random data.
645
99b2191e 646=item dump_these
647
648See L<Catalyst/dump_these> - ammends the session data structure to the list of
649dumped objects if session ID is defined.
650
9e447f9d 651=back
652
a92c8aeb 653=head1 USING SESSIONS DURING PREPARE
654
655The earliest point in time at which you may use the session data is after
656L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
657
658State plugins must set $c->session ID before C<prepare_action>, and during
659C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
660the store.
661
662 sub prepare_action {
663 my $c = shift;
664
665 # don't touch $c->session yet!
b1cd7d77 666
a92c8aeb 667 $c->NEXT::prepare_action( @_ );
668
669 $c->session; # this is OK
670 $c->sessionid; # this is also OK
671 }
672
9e447f9d 673=head1 CONFIGURATION
674
229a5b53 675 $c->config->{session} = {
676 expires => 1234,
677 };
9e447f9d 678
679All configuation parameters are provided in a hash reference under the
680C<session> key in the configuration hash.
681
682=over 4
683
684=item expires
685
686The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
687hours).
688
689=item verify_address
690
8c7e922c 691When true, C<<$c->request->address>> will be checked at prepare time. If it is
692not the same as the address that initiated the session, the session is deleted.
9e447f9d 693
68fd02ae 694=item flash_to_stash
695
696This option makes it easier to have actions behave the same whether they were
697forwarded to or redirected to. On prepare time it copies the contents of
698C<flash> (if any) to the stash.
699
9e447f9d 700=back
701
702=head1 SPECIAL KEYS
703
704The hash reference returned by C<< $c->session >> contains several keys which
705are automatically set:
706
707=over 4
708
709=item __expires
710
d44bc687 711This key no longer exists. This data is now saved elsewhere.
9e447f9d 712
713=item __updated
714
d44bc687 715The last time a session was saved to the store.
9e447f9d 716
717=item __created
718
719The time when the session was first created.
720
721=item __address
722
723The value of C<< $c->request->address >> at the time the session was created.
8c7e922c 724This value is only populated if C<verify_address> is true in the configuration.
9e447f9d 725
726=back
727
c80e9f04 728=head1 CAVEATS
729
a552e4b5 730=head2 Round the Robin Proxies
731
c80e9f04 732C<verify_address> could make your site inaccessible to users who are behind
733load balanced proxies. Some ISPs may give a different IP to each request by the
734same client due to this type of proxying. If addresses are verified these
735users' sessions cannot persist.
736
737To let these users access your site you can either disable address verification
738as a whole, or provide a checkbox in the login dialog that tells the server
739that it's OK for the address of the client to change. When the server sees that
740this box is checked it should delete the C<__address> sepcial key from the
741session hash when the hash is first created.
742
a552e4b5 743=head2 Race Conditions
744
745In this day and age where cleaning detergents and dutch football (not the
746american kind) teams roam the plains in great numbers, requests may happen
747simultaneously. This means that there is some risk of session data being
748overwritten, like this:
749
750=over 4
751
752=item 1.
753
754request a starts, request b starts, with the same session id
755
756=item 2.
757
758session data is loaded in request a
759
760=item 3.
761
762session data is loaded in request b
763
764=item 4.
765
766session data is changed in request a
767
768=item 5.
769
770request a finishes, session data is updated and written to store
771
772=item 6.
773
774request b finishes, session data is updated and written to store, overwriting
775changes by request a
776
777=back
778
779If this is a concern in your application, a soon to be developed locking
780solution is the only safe way to go. This will have a bigger overhead.
781
782For applications where any given user is only making one request at a time this
783plugin should be safe enough.
784
d45028d6 785=head1 AUTHORS
786
baa9db9c 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
799And countless other contributers from #catalyst. Thanks guys!
d45028d6 800
cc40ae4b 801=head1 COPYRIGHT & LICENSE
d45028d6 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
9e447f9d 807=cut
808
809