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