First hacks at turning into a Role. It's not yet working.
[catagits/Catalyst-Plugin-Session.git] / lib / Catalyst / Plugin / Session.pm
CommitLineData
9e447f9d 1#!/usr/bin/perl
2
3package Catalyst::Plugin::Session;
9e447f9d 4
2f0cb5b7 5use Moose::Role;
fe0d5ebe 6use MRO::Compat;
9e447f9d 7use Catalyst::Exception ();
9a9252c2 8use Digest ();
9use overload ();
d44bc687 10use Object::Signature ();
0466f6c7 11use Carp;
9e447f9d 12
c0c50a2c 13use namespace::clean -except => 'meta';
14
af1e4bc8 15our $VERSION = '0.25';
37160715 16
2f0cb5b7 17# used in delete_session
18my @session_data_accessors = qw/
19 _sessionid
20 _session
21 _session_expires
22 _extended_session_expires
23 _session_data_sig
24 _flash
25 _flash_keep_keys
26 _flash_key_hashes
27 _tried_loading_session_id
28 _tried_loading_session_data
29 _tried_loading_session_expires
30 _tried_loading_flash_data
31 /;
32
33has '_session_delete_reason' => ( is => 'rw');
34foreach (@session_data_accessors) {
35 has $_ => ( is => 'rw');
36}
9a9252c2 37
2f0cb5b7 38
39before 'setup_finalize' => sub {
40 my $c = shift;
9a9252c2 41
42 $c->check_session_plugin_requirements;
43 $c->setup_session;
44
45 return $c;
2f0cb5b7 46};
9e447f9d 47
48sub check_session_plugin_requirements {
9a9252c2 49 my $c = shift;
9e447f9d 50
9a9252c2 51 unless ( $c->isa("Catalyst::Plugin::Session::State")
52 && $c->isa("Catalyst::Plugin::Session::Store") )
53 {
54 my $err =
55 ( "The Session plugin requires both Session::State "
56 . "and Session::Store plugins to be used as well." );
9e447f9d 57
9a9252c2 58 $c->log->fatal($err);
59 Catalyst::Exception->throw($err);
60 }
9e447f9d 61}
62
63sub setup_session {
9a9252c2 64 my $c = shift;
9e447f9d 65
9a9252c2 66 my $cfg = ( $c->config->{session} ||= {} );
9e447f9d 67
9a9252c2 68 %$cfg = (
69 expires => 7200,
33641c1d 70 verify_address => 0,
06c621b5 71 verify_user_agent => 0,
9a9252c2 72 %$cfg,
73 );
9e447f9d 74
fe0d5ebe 75 $c->maybe::next::method();
9e447f9d 76}
77
2f0cb5b7 78before 'prepare_action' => sub {
19c130c2 79 my $c = shift;
80
4207ce8d 81 if ( $c->config->{session}{flash_to_stash}
e5b2372a 82 and $c->sessionid
4207ce8d 83 and my $flash_data = $c->flash )
84 {
19c130c2 85 @{ $c->stash }{ keys %$flash_data } = values %$flash_data;
86 }
87
2f0cb5b7 88};
19c130c2 89
2f0cb5b7 90before 'finalize_headers' => sub {
9a9252c2 91 my $c = shift;
9e447f9d 92
d3c97126 93 # fix cookie before we send headers
94 $c->_save_session_expires;
2a45c03b 95
2f0cb5b7 96};
8f236527 97
2f0cb5b7 98before 'finalize_body' => sub {
d3c97126 99 my $c = shift;
d3c97126 100
92eaec32 101 # We have to finalize our session *before* $c->engine->finalize_xxx is called,
102 # because we do not want to send the HTTP response before the session is stored/committed to
103 # the session database (or whatever Session::Store you use).
d3c97126 104 $c->finalize_session;
92eaec32 105
2f0cb5b7 106};
d3c97126 107
8f236527 108sub finalize_session {
109 my $c = shift;
110
fe0d5ebe 111 $c->maybe::next::method(@_);
49727697 112
9760b704 113 $c->_save_session_id;
9b0fa2a6 114 $c->_save_session;
115 $c->_save_flash;
116
260b14c4 117 $c->_clear_session_instance_data;
9b0fa2a6 118}
119
b92ce604 120sub _save_session_id {
121 my $c = shift;
23a2bf16 122
123 # we already called set when allocating
124 # no need to tell the state plugins anything new
b92ce604 125}
126
1dbf4cd5 127sub _save_session_expires {
9b0fa2a6 128 my $c = shift;
4207ce8d 129
260b14c4 130 if ( defined($c->_session_expires) ) {
131 my $expires = $c->session_expires; # force extension
132
1dbf4cd5 133 my $sid = $c->sessionid;
134 $c->store_session_data( "expires:$sid" => $expires );
1dbf4cd5 135 }
136}
6687905d 137
1dbf4cd5 138sub _save_session {
139 my $c = shift;
7a02371f 140
1dbf4cd5 141 if ( my $session_data = $c->_session ) {
d44bc687 142
1dbf4cd5 143 no warnings 'uninitialized';
144 if ( Object::Signature::signature($session_data) ne
145 $c->_session_data_sig )
146 {
147 $session_data->{__updated} = time();
148 my $sid = $c->sessionid;
149 $c->store_session_data( "session:$sid" => $session_data );
ea972e9a 150 }
9a9252c2 151 }
9b0fa2a6 152}
9a9252c2 153
9b0fa2a6 154sub _save_flash {
155 my $c = shift;
156
1dbf4cd5 157 if ( my $flash_data = $c->_flash ) {
4207ce8d 158
1dbf4cd5 159 my $hashes = $c->_flash_key_hashes || {};
160 my $keep = $c->_flash_keep_keys || {};
161 foreach my $key ( keys %$hashes ) {
162 if ( !exists $keep->{$key} and Object::Signature::signature( \$flash_data->{$key} ) eq $hashes->{$key} ) {
163 delete $flash_data->{$key};
23fbca00 164 }
ea972e9a 165 }
af1e4bc8 166
1dbf4cd5 167 my $sid = $c->sessionid;
168
eb250519 169 my $session_data = $c->_session;
1dbf4cd5 170 if (%$flash_data) {
eb250519 171 $session_data->{__flash} = $flash_data;
1dbf4cd5 172 }
173 else {
eb250519 174 delete $session_data->{__flash};
1dbf4cd5 175 }
eb250519 176 $c->_session($session_data);
177 $c->_save_session;
9b0fa2a6 178 }
9e447f9d 179}
180
e5b2372a 181sub _load_session_expires {
182 my $c = shift;
b92ce604 183 return $c->_session_expires if $c->_tried_loading_session_expires;
1dbf4cd5 184 $c->_tried_loading_session_expires(1);
e5b2372a 185
186 if ( my $sid = $c->sessionid ) {
187 my $expires = $c->get_session_data("expires:$sid") || 0;
188
189 if ( $expires >= time() ) {
260b14c4 190 $c->_session_expires( $expires );
191 return $expires;
e5b2372a 192 } else {
193 $c->delete_session( "session expired" );
194 return 0;
195 }
e5b2372a 196 }
1dbf4cd5 197
198 return;
e5b2372a 199}
200
b7acf64e 201sub _load_session {
202 my $c = shift;
b92ce604 203 return $c->_session if $c->_tried_loading_session_data;
1dbf4cd5 204 $c->_tried_loading_session_data(1);
b7acf64e 205
e5b2372a 206 if ( my $sid = $c->sessionid ) {
260b14c4 207 if ( $c->_load_session_expires ) { # > 0
0974ac06 208
2e412459 209 my $session_data = $c->get_session_data("session:$sid") || return;
6687905d 210 $c->_session($session_data);
3f182468 211
6687905d 212 no warnings 'uninitialized'; # ne __address
213 if ( $c->config->{session}{verify_address}
214 && $session_data->{__address} ne $c->request->address )
215 {
216 $c->log->warn(
217 "Deleting session $sid due to address mismatch ("
218 . $session_data->{__address} . " != "
47ca362e 219 . $c->request->address . ")"
6687905d 220 );
221 $c->delete_session("address mismatch");
222 return;
223 }
06c621b5 224 if ( $c->config->{session}{verify_user_agent}
225 && $session_data->{__user_agent} ne $c->request->user_agent )
226 {
227 $c->log->warn(
228 "Deleting session $sid due to user agent mismatch ("
229 . $session_data->{__user_agent} . " != "
230 . $c->request->user_agent . ")"
231 );
232 $c->delete_session("user agent mismatch");
233 return;
234 }
4207ce8d 235
6687905d 236 $c->log->debug(qq/Restored session "$sid"/) if $c->debug;
168d6819 237 $c->_session_data_sig( Object::Signature::signature($session_data) ) if $session_data;
6687905d 238 $c->_expire_session_keys;
4207ce8d 239
6687905d 240 return $session_data;
3f182468 241 }
9a9252c2 242 }
29d15411 243
4207ce8d 244 return;
b7acf64e 245}
9a9252c2 246
9b0fa2a6 247sub _load_flash {
248 my $c = shift;
b92ce604 249 return $c->_flash if $c->_tried_loading_flash_data;
1dbf4cd5 250 $c->_tried_loading_flash_data(1);
9b0fa2a6 251
e5b2372a 252 if ( my $sid = $c->sessionid ) {
eb250519 253
254 my $session_data = $c->session;
255 $c->_flash($session_data->{__flash});
256
257 if ( my $flash_data = $c->_flash )
4207ce8d 258 {
f4d79f85 259 $c->_flash_key_hashes({ map { $_ => Object::Signature::signature( \$flash_data->{$_} ) } keys %$flash_data });
af1e4bc8 260
9b0fa2a6 261 return $flash_data;
262 }
263 }
264
1dbf4cd5 265 return;
9b0fa2a6 266}
267
d44bc687 268sub _expire_session_keys {
b7acf64e 269 my ( $c, $data ) = @_;
270
271 my $now = time;
272
e5b2372a 273 my $expire_times = ( $data || $c->_session || {} )->{__expire_keys} || {};
274 foreach my $key ( grep { $expire_times->{$_} < $now } keys %$expire_times ) {
b7acf64e 275 delete $c->_session->{$key};
e5b2372a 276 delete $expire_times->{$key};
b7acf64e 277 }
9e447f9d 278}
279
260b14c4 280sub _clear_session_instance_data {
281 my $c = shift;
282 $c->$_(undef) for @session_data_accessors;
fe0d5ebe 283 $c->maybe::next::method(@_); # allow other plugins to hook in on this
260b14c4 284}
285
0ade68bd 286sub change_session_id {
287 my $c = shift;
288
289 my $sessiondata = $c->session;
290 my $oldsid = $c->sessionid;
291 my $newsid = $c->create_session_id;
af1e4bc8 292
0ade68bd 293 if ($oldsid) {
294 $c->log->debug(qq/change_sessid: deleting session data from "$oldsid"/) if $c->debug;
295 $c->delete_session_data("${_}:${oldsid}") for qw/session expires flash/;
296 }
297
298 $c->log->debug(qq/change_sessid: storing session data to "$newsid"/) if $c->debug;
299 $c->store_session_data( "session:$newsid" => $sessiondata );
300
af1e4bc8 301 return $newsid;
0ade68bd 302}
303
9e447f9d 304sub delete_session {
9a9252c2 305 my ( $c, $msg ) = @_;
9e447f9d 306
49727697 307 $c->log->debug("Deleting session" . ( defined($msg) ? "($msg)" : '(no reason given)') ) if $c->debug;
b92ce604 308
9a9252c2 309 # delete the session data
b92ce604 310 if ( my $sid = $c->sessionid ) {
311 $c->delete_session_data("${_}:${sid}") for qw/session expires flash/;
c6eaa93a 312 $c->delete_session_id($sid);
b92ce604 313 }
e5b2372a 314
9a9252c2 315 # reset the values in the context object
ab634fee 316 # see the BEGIN block
260b14c4 317 $c->_clear_session_instance_data;
6687905d 318
29d15411 319 $c->_session_delete_reason($msg);
320}
321
322sub session_delete_reason {
323 my $c = shift;
324
e5b2372a 325 $c->session_is_valid; # check that it was loaded
29d15411 326
4207ce8d 327 $c->_session_delete_reason(@_);
9e447f9d 328}
329
6687905d 330sub session_expires {
e5b2372a 331 my $c = shift;
6687905d 332
260b14c4 333 if ( defined( my $expires = $c->_extended_session_expires ) ) {
e5b2372a 334 return $expires;
1dbf4cd5 335 } elsif ( defined( $expires = $c->_load_session_expires ) ) {
260b14c4 336 return $c->extend_session_expires( $expires );
e5b2372a 337 } else {
1dbf4cd5 338 return 0;
e5b2372a 339 }
340}
6687905d 341
e5b2372a 342sub extend_session_expires {
343 my ( $c, $expires ) = @_;
260b14c4 344 $c->_extended_session_expires( my $updated = $c->calculate_extended_session_expires( $expires ) );
4c79feb6 345 $c->extend_session_id( $c->sessionid, $updated );
e5b2372a 346 return $updated;
347}
6687905d 348
e5b2372a 349sub calculate_initial_session_expires {
350 my $c = shift;
351 return ( time() + $c->config->{session}{expires} );
352}
353
354sub calculate_extended_session_expires {
355 my ( $c, $prev ) = @_;
356 $c->calculate_initial_session_expires;
357}
358
359sub reset_session_expires {
360 my ( $c, $sid ) = @_;
af1e4bc8 361
260b14c4 362 my $exp = $c->calculate_initial_session_expires;
363 $c->_session_expires( $exp );
364 $c->_extended_session_expires( $exp );
e5b2372a 365 $exp;
6687905d 366}
367
0974ac06 368sub sessionid {
4207ce8d 369 my $c = shift;
af1e4bc8 370
1dbf4cd5 371 return $c->_sessionid || $c->_load_sessionid;
372}
373
374sub _load_sessionid {
375 my $c = shift;
376 return if $c->_tried_loading_session_id;
377 $c->_tried_loading_session_id(1);
378
379 if ( defined( my $sid = $c->get_session_id ) ) {
380 if ( $c->validate_session_id($sid) ) {
ec299c02 381 # temporarily set the inner key, so that validation will work
382 $c->_sessionid($sid);
1dbf4cd5 383 return $sid;
384 } else {
385 my $err = "Tried to set invalid session ID '$sid'";
386 $c->log->error($err);
387 Catalyst::Exception->throw($err);
4207ce8d 388 }
389 }
1dbf4cd5 390
391 return;
e5b2372a 392}
393
394sub session_is_valid {
395 my $c = shift;
4207ce8d 396
b92ce604 397 # force a check for expiry, but also __address, etc
398 if ( $c->_load_session ) {
e5b2372a 399 return 1;
400 } else {
401 return;
402 }
0974ac06 403}
404
405sub validate_session_id {
4207ce8d 406 my ( $c, $sid ) = @_;
0974ac06 407
4207ce8d 408 $sid and $sid =~ /^[a-f\d]+$/i;
0974ac06 409}
410
9e447f9d 411sub session {
9a9252c2 412 my $c = shift;
9e447f9d 413
29d15411 414 $c->_session || $c->_load_session || do {
1dbf4cd5 415 $c->create_session_id_if_needed;
29d15411 416 $c->initialize_session_data;
4207ce8d 417 };
9e447f9d 418}
419
f4d79f85 420sub keep_flash {
421 my ( $c, @keys ) = @_;
2e412459 422 my $href = $c->_flash_keep_keys || $c->_flash_keep_keys({});
423 (@{$href}{@keys}) = ((undef) x @keys);
f4d79f85 424}
425
af1e4bc8 426sub _flash_data {
873f7011 427 my $c = shift;
78476ce0 428 $c->_flash || $c->_load_flash || do {
1dbf4cd5 429 $c->create_session_id_if_needed;
78476ce0 430 $c->_flash( {} );
c9396824 431 };
432}
433
434sub _set_flash {
435 my $c = shift;
436 if (@_) {
437 my $items = @_ > 1 ? {@_} : $_[0];
438 croak('flash takes a hash or hashref') unless ref $items;
439 @{ $c->_flash }{ keys %$items } = values %$items;
1dbf4cd5 440 }
873f7011 441}
442
c9396824 443sub flash {
444 my $c = shift;
445 $c->_flash_data;
f6009cac 446 $c->_set_flash(@_);
447 return $c->_flash;
c9396824 448}
449
49727697 450sub clear_flash {
451 my $c = shift;
af1e4bc8 452
5a1f6ed4 453 #$c->delete_session_data("flash:" . $c->sessionid); # should this be in here? or delayed till finalization?
49727697 454 $c->_flash_key_hashes({});
5a1f6ed4 455 $c->_flash_keep_keys({});
49727697 456 $c->_flash({});
457}
458
b7acf64e 459sub session_expire_key {
460 my ( $c, %keys ) = @_;
461
462 my $now = time;
4207ce8d 463 @{ $c->session->{__expire_keys} }{ keys %keys } =
464 map { $now + $_ } values %keys;
b7acf64e 465}
466
9e447f9d 467sub initialize_session_data {
9a9252c2 468 my $c = shift;
9e447f9d 469
9a9252c2 470 my $now = time;
9e447f9d 471
4207ce8d 472 return $c->_session(
473 {
474 __created => $now,
475 __updated => $now,
476
477 (
478 $c->config->{session}{verify_address}
f8f81744 479 ? ( __address => $c->request->address||'' )
4207ce8d 480 : ()
481 ),
06c621b5 482 (
483 $c->config->{session}{verify_user_agent}
f8f81744 484 ? ( __user_agent => $c->request->user_agent||'' )
06c621b5 485 : ()
486 ),
4207ce8d 487 }
488 );
9e447f9d 489}
490
9e447f9d 491sub generate_session_id {
492 my $c = shift;
493
494 my $digest = $c->_find_digest();
495 $digest->add( $c->session_hash_seed() );
496 return $digest->hexdigest;
497}
498
1dbf4cd5 499sub create_session_id_if_needed {
78476ce0 500 my $c = shift;
1dbf4cd5 501 $c->create_session_id unless $c->sessionid;
502}
78476ce0 503
1dbf4cd5 504sub create_session_id {
505 my $c = shift;
af1e4bc8 506
e5b2372a 507 my $sid = $c->generate_session_id;
78476ce0 508
e5b2372a 509 $c->log->debug(qq/Created session "$sid"/) if $c->debug;
78476ce0 510
e5b2372a 511 $c->_sessionid($sid);
512 $c->reset_session_expires;
513 $c->set_session_id($sid);
514
515 return $sid;
78476ce0 516}
517
9e447f9d 518my $counter;
9a9252c2 519
9e447f9d 520sub session_hash_seed {
9a9252c2 521 my $c = shift;
522
523 return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
9e447f9d 524}
525
526my $usable;
9a9252c2 527
9e447f9d 528sub _find_digest () {
9a9252c2 529 unless ($usable) {
d44bc687 530 foreach my $alg (qw/SHA-1 SHA-256 MD5/) {
4207ce8d 531 if ( eval { Digest->new($alg) } ) {
5faaa4b0 532 $usable = $alg;
533 last;
534 }
7d139eeb 535 }
4207ce8d 536 Catalyst::Exception->throw(
9a9252c2 537 "Could not find a suitable Digest module. Please install "
4207ce8d 538 . "Digest::SHA1, Digest::SHA, or Digest::MD5" )
539 unless $usable;
9a9252c2 540 }
9e447f9d 541
542 return Digest->new($usable);
543}
544
99b2191e 545sub dump_these {
546 my $c = shift;
547
548 (
fe0d5ebe 549 $c->maybe::next::method(),
99b2191e 550
551 $c->sessionid
552 ? ( [ "Session ID" => $c->sessionid ], [ Session => $c->session ], )
553 : ()
554 );
555}
556
e5b2372a 557
fe0d5ebe 558sub get_session_id { shift->maybe::next::method(@_) }
559sub set_session_id { shift->maybe::next::method(@_) }
560sub delete_session_id { shift->maybe::next::method(@_) }
561sub extend_session_id { shift->maybe::next::method(@_) }
e5b2372a 562
9e447f9d 563__PACKAGE__;
564
565__END__
566
567=pod
568
569=head1 NAME
570
7048c24e 571Catalyst::Plugin::Session - Generic Session plugin - ties together server side storage and client side state required to maintain session data.
9e447f9d 572
573=head1 SYNOPSIS
574
8f0b4c16 575 # To get sessions to "just work", all you need to do is use these plugins:
576
577 use Catalyst qw/
578 Session
579 Session::Store::FastMmap
580 Session::State::Cookie
581 /;
582
7048c24e 583 # you can replace Store::FastMmap with Store::File - both have sensible
584 # default configurations (see their docs for details)
8f0b4c16 585
7048c24e 586 # more complicated backends are available for other scenarios (DBI storage,
587 # etc)
8f0b4c16 588
589
590 # after you've loaded the plugins you can save session data
591 # For example, if you are writing a shopping cart, it could be implemented
592 # like this:
9e447f9d 593
229a5b53 594 sub add_item : Local {
595 my ( $self, $c ) = @_;
596
597 my $item_id = $c->req->param("item");
598
8f0b4c16 599 # $c->session is a hash ref, a bit like $c->stash
600 # the difference is that it' preserved across requests
229a5b53 601
602 push @{ $c->session->{items} }, $item_id;
603
604 $c->forward("MyView");
605 }
606
607 sub display_items : Local {
608 my ( $self, $c ) = @_;
609
610 # values in $c->session are restored
611 $c->stash->{items_to_display} =
8f0b4c16 612 [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
229a5b53 613
614 $c->forward("MyView");
615 }
616
9e447f9d 617=head1 DESCRIPTION
618
619The Session plugin is the base of two related parts of functionality required
620for session management in web applications.
621
622The first part, the State, is getting the browser to repeat back a session key,
623so that the web application can identify the client and logically string
624several requests together into a session.
625
626The second part, the Store, deals with the actual storage of information about
627the client. This data is stored so that the it may be revived for every request
628made by the same client.
629
630This plugin links the two pieces together.
631
2a323f6f 632=head1 RECOMENDED BACKENDS
8f0b4c16 633
634=over 4
635
636=item Session::State::Cookie
637
638The only really sane way to do state is using cookies.
639
640=item Session::Store::File
641
642A portable backend, based on Cache::File.
643
644=item Session::Store::FastMmap
645
646A fast and flexible backend, based on Cache::FastMmap.
647
648=back
649
9e447f9d 650=head1 METHODS
651
652=over 4
653
654=item sessionid
655
656An accessor for the session ID value.
657
658=item session
659
660Returns a hash reference that might contain unserialized values from previous
661requests in the same session, and whose modified value will be saved for future
662requests.
663
664This method will automatically create a new session and session ID if none
665exists.
666
ab634fee 667=item session_expires
668
669=item session_expires $reset
670
671This method returns the time when the current session will expire, or 0 if
672there is no current session. If there is a session and it already expired, it
673will delete the session and return 0 as well.
674
675If the C<$reset> parameter is true, and there is a session ID the expiry time
676will be reset to the current time plus the time to live (see
677L</CONFIGURATION>). This is used when creating a new session.
678
07e714d2 679=item flash
680
681This is like Ruby on Rails' flash data structure. Think of it as a stash that
44ab6d1c 682lasts for longer than one request, letting you redirect instead of forward.
683
684The flash data will be cleaned up only on requests on which actually use
685$c->flash (thus allowing multiple redirections), and the policy is to delete
bf6bd311 686all the keys which haven't changed since the flash data was loaded at the end
687of every request.
07e714d2 688
689 sub moose : Local {
690 my ( $self, $c ) = @_;
691
692 $c->flash->{beans} = 10;
693 $c->response->redirect( $c->uri_for("foo") );
694 }
695
696 sub foo : Local {
697 my ( $self, $c ) = @_;
698
699 my $value = $c->flash->{beans};
700
701 # ...
702
703 $c->response->redirect( $c->uri_for("bar") );
704 }
705
706 sub bar : Local {
707 my ( $self, $c ) = @_;
708
709 if ( exists $c->flash->{beans} ) { # false
af1e4bc8 710
07e714d2 711 }
712 }
713
49727697 714=item clear_flash
715
716Zap all the keys in the flash regardless of their current state.
717
bf6bd311 718=item keep_flash @keys
719
1961343c 720If you want to keep a flash key for the next request too, even if it hasn't
bf6bd311 721changed, call C<keep_flash> and pass in the keys as arguments.
722
fffeb18f 723=item delete_session REASON
724
725This method is used to invalidate a session. It takes an optional parameter
726which will be saved in C<session_delete_reason> if provided.
727
eb250519 728NOTE: This method will B<also> delete your flash data.
729
9e447f9d 730=item session_delete_reason
731
732This accessor contains a string with the reason a session was deleted. Possible
733values include:
734
735=over 4
736
737=item *
738
739C<address mismatch>
740
741=item *
742
743C<session expired>
744
745=back
746
b7acf64e 747=item session_expire_key $key, $ttl
748
749Mark a key to expire at a certain time (only useful when shorter than the
750expiry time for the whole session).
751
752For example:
753
754 __PACKAGE__->config->{session}{expires} = 1000000000000; # forever
755
756 # later
757
758 $c->session_expire_key( __user => 3600 );
759
760Will make the session data survive, but the user will still be logged out after
761an hour.
762
763Note that these values are not auto extended.
764
0ade68bd 765=item change_session_id
766
767By calling this method you can force a session id change while keeping all
768session data. This method might come handy when you are paranoid about some
769advanced variations of session fixation attack.
770
771If you want to prevent this session fixation scenario:
772
773 0) let us have WebApp with anonymous and authenticated parts
af1e4bc8 774 1) a hacker goes to vulnerable WebApp and gets a real sessionid,
0ade68bd 775 just by browsing anonymous part of WebApp
776 2) the hacker inserts (somehow) this values into a cookie in victim's browser
777 3) after the victim logs into WebApp the hacker can enter his/her session
778
779you should call change_session_id in your login controller like this:
780
781 if ($c->authenticate( { username => $user, password => $pass } )) {
782 # login OK
783 $c->change_session_id;
784 ...
785 } else {
786 # login FAILED
787 ...
788 }
789
8f0b4c16 790=back
791
10c72079 792=head1 INTERNAL METHODS
8f0b4c16 793
794=over 4
795
9e447f9d 796=item setup
797
798This method is extended to also make calls to
799C<check_session_plugin_requirements> and C<setup_session>.
800
801=item check_session_plugin_requirements
802
803This method ensures that a State and a Store plugin are also in use by the
804application.
805
806=item setup_session
807
808This method populates C<< $c->config->{session} >> with the default values
809listed in L</CONFIGURATION>.
810
811=item prepare_action
812
bfa4f9cc 813This method is extended.
68fd02ae 814
bfa4f9cc 815Its only effect is if the (off by default) C<flash_to_stash> configuration
68fd02ae 816parameter is on - then it will copy the contents of the flash to the stash at
817prepare time.
9e447f9d 818
ccc77553 819=item finalize_headers
9e447f9d 820
d3c97126 821This method is extended and will extend the expiry time before sending
822the response.
823
92eaec32 824=item finalize_body
d3c97126 825
92eaec32 826This method is extended and will call finalize_session before the other
827finalize_body methods run. Here we persist the session data if a session exists.
9e447f9d 828
9e447f9d 829=item initialize_session_data
830
831This method will initialize the internal structure of the session, and is
832called by the C<session> method if appropriate.
833
68fd02ae 834=item create_session_id
835
bfa4f9cc 836Creates a new session ID using C<generate_session_id> if there is no session ID
68fd02ae 837yet.
838
ab634fee 839=item validate_session_id SID
840
841Make sure a session ID is of the right format.
842
843This currently ensures that the session ID string is any amount of case
844insensitive hexadecimal characters.
845
229a5b53 846=item generate_session_id
847
848This method will return a string that can be used as a session ID. It is
849supposed to be a reasonably random string with enough bits to prevent
850collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
bfa4f9cc 851MD5 or SHA-256, depending on the availability of these modules.
229a5b53 852
853=item session_hash_seed
854
855This method is actually rather internal to generate_session_id, but should be
856overridable in case you want to provide more random data.
857
858Currently it returns a concatenated string which contains:
859
860=over 4
861
7048c24e 862=item * A counter
229a5b53 863
7048c24e 864=item * The current time
229a5b53 865
7048c24e 866=item * One value from C<rand>.
229a5b53 867
7048c24e 868=item * The stringified value of a newly allocated hash reference
229a5b53 869
7048c24e 870=item * The stringified value of the Catalyst context object
229a5b53 871
872=back
873
bfa4f9cc 874in the hopes that those combined values are entropic enough for most uses. If
229a5b53 875this is not the case you can replace C<session_hash_seed> with e.g.
876
877 sub session_hash_seed {
878 open my $fh, "<", "/dev/random";
879 read $fh, my $bytes, 20;
880 close $fh;
881 return $bytes;
882 }
883
884Or even more directly, replace C<generate_session_id>:
885
886 sub generate_session_id {
887 open my $fh, "<", "/dev/random";
888 read $fh, my $bytes, 20;
889 close $fh;
890 return unpack("H*", $bytes);
891 }
892
893Also have a look at L<Crypt::Random> and the various openssl bindings - these
894modules provide APIs for cryptographically secure random data.
895
8f236527 896=item finalize_session
897
898Clean up the session during C<finalize>.
899
900This clears the various accessors after saving to the store.
901
99b2191e 902=item dump_these
903
904See L<Catalyst/dump_these> - ammends the session data structure to the list of
905dumped objects if session ID is defined.
906
d3c97126 907
908=item calculate_extended_session_expires
909
910=item calculate_initial_session_expires
911
912=item create_session_id_if_needed
913
914=item delete_session_id
915
916=item extend_session_expires
917
918=item extend_session_id
919
920=item get_session_id
921
922=item reset_session_expires
923
924=item session_is_valid
925
926=item set_session_id
927
9e447f9d 928=back
929
a92c8aeb 930=head1 USING SESSIONS DURING PREPARE
931
932The earliest point in time at which you may use the session data is after
933L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
934
935State plugins must set $c->session ID before C<prepare_action>, and during
936C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
937the store.
938
7048c24e 939 sub prepare_action {
940 my $c = shift;
a92c8aeb 941
7048c24e 942 # don't touch $c->session yet!
b1cd7d77 943
7048c24e 944 $c->NEXT::prepare_action( @_ );
a92c8aeb 945
7048c24e 946 $c->session; # this is OK
947 $c->sessionid; # this is also OK
948 }
a92c8aeb 949
9e447f9d 950=head1 CONFIGURATION
951
229a5b53 952 $c->config->{session} = {
953 expires => 1234,
954 };
9e447f9d 955
956All configuation parameters are provided in a hash reference under the
957C<session> key in the configuration hash.
958
959=over 4
960
961=item expires
962
963The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
964hours).
965
966=item verify_address
967
8c7e922c 968When true, C<<$c->request->address>> will be checked at prepare time. If it is
969not the same as the address that initiated the session, the session is deleted.
9e447f9d 970
33641c1d 971Defaults to false.
972
06c621b5 973=item verify_user_agent
974
975When true, C<<$c->request->user_agent>> will be checked at prepare time. If it
af1e4bc8 976is not the same as the user agent that initiated the session, the session is
06c621b5 977deleted.
978
979Defaults to false.
980
68fd02ae 981=item flash_to_stash
982
983This option makes it easier to have actions behave the same whether they were
984forwarded to or redirected to. On prepare time it copies the contents of
985C<flash> (if any) to the stash.
986
9e447f9d 987=back
988
989=head1 SPECIAL KEYS
990
991The hash reference returned by C<< $c->session >> contains several keys which
992are automatically set:
993
994=over 4
995
996=item __expires
997
ab634fee 998This key no longer exists. Use C<session_expires> instead.
9e447f9d 999
1000=item __updated
1001
d44bc687 1002The last time a session was saved to the store.
9e447f9d 1003
1004=item __created
1005
1006The time when the session was first created.
1007
1008=item __address
1009
1010The value of C<< $c->request->address >> at the time the session was created.
8c7e922c 1011This value is only populated if C<verify_address> is true in the configuration.
9e447f9d 1012
06c621b5 1013=item __user_agent
1014
e79a686c 1015The value of C<< $c->request->user_agent >> at the time the session was created.
06c621b5 1016This value is only populated if C<verify_user_agent> is true in the configuration.
1017
9e447f9d 1018=back
1019
c80e9f04 1020=head1 CAVEATS
1021
a552e4b5 1022=head2 Round the Robin Proxies
1023
c80e9f04 1024C<verify_address> could make your site inaccessible to users who are behind
1025load balanced proxies. Some ISPs may give a different IP to each request by the
1026same client due to this type of proxying. If addresses are verified these
1027users' sessions cannot persist.
1028
1029To let these users access your site you can either disable address verification
1030as a whole, or provide a checkbox in the login dialog that tells the server
1031that it's OK for the address of the client to change. When the server sees that
bfa4f9cc 1032this box is checked it should delete the C<__address> special key from the
c80e9f04 1033session hash when the hash is first created.
1034
a552e4b5 1035=head2 Race Conditions
1036
bfa4f9cc 1037In this day and age where cleaning detergents and Dutch football (not the
1038American kind) teams roam the plains in great numbers, requests may happen
a552e4b5 1039simultaneously. This means that there is some risk of session data being
1040overwritten, like this:
1041
1042=over 4
1043
1044=item 1.
1045
bfa4f9cc 1046request a starts, request b starts, with the same session ID
a552e4b5 1047
1048=item 2.
1049
1050session data is loaded in request a
1051
1052=item 3.
1053
1054session data is loaded in request b
1055
1056=item 4.
1057
1058session data is changed in request a
1059
1060=item 5.
1061
1062request a finishes, session data is updated and written to store
1063
1064=item 6.
1065
1066request b finishes, session data is updated and written to store, overwriting
1067changes by request a
1068
1069=back
1070
bfa4f9cc 1071If this is a concern in your application, a soon-to-be-developed locking
a552e4b5 1072solution is the only safe way to go. This will have a bigger overhead.
1073
1074For applications where any given user is only making one request at a time this
1075plugin should be safe enough.
1076
d45028d6 1077=head1 AUTHORS
1078
7048c24e 1079Andy Grundman
baa9db9c 1080
7048c24e 1081Christian Hansen
baa9db9c 1082
2842d938 1083Yuval Kogman, C<nothingmuch@woobling.org>
baa9db9c 1084
7048c24e 1085Sebastian Riedel
baa9db9c 1086
2842d938 1087Tomas Doran (t0m) C<bobtfish@bobtfish.net> (current maintainer)
1088
1089Sergio Salvi
1090
af1e4bc8 1091kmx C<kmx@volny.cz>
1092
baa9db9c 1093And countless other contributers from #catalyst. Thanks guys!
d45028d6 1094
cc40ae4b 1095=head1 COPYRIGHT & LICENSE
d45028d6 1096
7048c24e 1097 Copyright (c) 2005 the aforementioned authors. All rights
1098 reserved. This program is free software; you can redistribute
1099 it and/or modify it under the same terms as Perl itself.
d45028d6 1100
9e447f9d 1101=cut
1102
1103