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