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