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