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