3 package Catalyst::Plugin::Session;
4 use base qw/Class::Accessor::Fast/;
10 use Catalyst::Exception ();
14 our $VERSION = "0.02";
17 __PACKAGE__->mk_accessors(qw/_sessionid _session _session_delete_reason _flash _flash_stale_keys/);
25 $c->check_session_plugin_requirements;
31 sub check_session_plugin_requirements {
34 unless ( $c->isa("Catalyst::Plugin::Session::State")
35 && $c->isa("Catalyst::Plugin::Session::Store") )
38 ( "The Session plugin requires both Session::State "
39 . "and Session::Store plugins to be used as well." );
42 Catalyst::Exception->throw($err);
49 my $cfg = ( $c->config->{session} ||= {} );
57 $c->NEXT::setup_session();
63 if ( $c->config->{session}{flash_to_stash} and $c->_sessionid and my $flash_data = $c->flash ) {
64 @{ $c->stash }{ keys %$flash_data } = values %$flash_data;
67 $c->NEXT::prepare_action(@_);
76 $c->NEXT::finalize(@_);
82 if ( my $sid = $c->_sessionid ) {
83 if ( my $session_data = $c->_session ) {
85 # all sessions are extended at the end of the request
87 @{ $session_data }{qw/__updated __expires/} =
88 ( $now, $c->config->{session}{expires} + $now );
90 $c->store_session_data( "session:$sid", $session_data );
98 if ( my $sid = $c->_sessionid ) {
99 if ( my $flash_data = $c->_flash ) {
100 if ( %$flash_data ) { # damn 'my' declarations
101 delete @{ $flash_data }{ @{ $c->_flash_stale_keys || [] } };
102 $c->store_session_data( "flash:$sid", $flash_data );
105 $c->delete_session_data( "flash:$sid" );
113 if ( my $sid = $c->_sessionid ) {
114 no warnings 'uninitialized'; # ne __address
116 my $session_data = $c->_session || $c->_session( $c->get_session_data( "session:$sid" ) );
117 if ( !$session_data or $session_data->{__expires} < time ) {
120 $c->log->debug("Deleting session $sid (expired)") if $c->debug;
121 $c->delete_session("session expired");
123 elsif ($c->config->{session}{verify_address}
124 && $session_data->{__address} ne $c->request->address )
127 "Deleting session $sid due to address mismatch ("
128 . $session_data->{__address} . " != "
129 . $c->request->address . ")",
131 $c->delete_session("address mismatch");
134 $c->log->debug(qq/Restored session "$sid"/) if $c->debug;
137 $c->_expire_ession_keys;
139 return $session_data;
148 if ( my $sid = $c->_sessionid ) {
149 if ( my $flash_data = $c->_flash || $c->_flash( $c->get_session_data( "flash:$sid" ) ) ) {
150 $c->_flash_stale_keys([ keys %$flash_data ]);
158 sub _expire_ession_keys {
159 my ( $c, $data ) = @_;
163 my $expiry = ($data || $c->_session || {})->{__expire_keys} || {};
164 foreach my $key (grep { $expiry->{$_} < $now } keys %$expiry ) {
165 delete $c->_session->{$key};
166 delete $expiry->{$key};
171 my ( $c, $msg ) = @_;
173 # delete the session data
174 my $sid = $c->_sessionid || return;
175 $c->delete_session_data( "session:$sid" );
177 # reset the values in the context object
179 $c->_sessionid(undef);
180 $c->_session_delete_reason($msg);
183 sub session_delete_reason {
186 $c->_load_session if ( $c->_sessionid && !$c->_session ); # must verify session data
188 $c->_session_delete_reason( @_ );
195 if ( $c->validate_session_id( my $sid = shift ) ) {
196 $c->_sessionid( $sid );
197 return unless defined wantarray;
199 my $err = "Tried to set invalid session ID '$sid'";
200 $c->log->error( $err );
201 Catalyst::Exception->throw( $err );
205 $c->_load_session if ( $c->_sessionid && !$c->_session ); # must verify session data
207 return $c->_sessionid;
210 sub validate_session_id {
211 my ( $c, $sid ) = @_;
213 $sid and $sid =~ /^[a-f\d]+$/i;
219 $c->_session || $c->_load_session || do {
220 $c->create_session_id;
222 $c->initialize_session_data;
228 $c->_flash || $c->_load_flash || do {
229 $c->create_session_id;
234 sub session_expire_key {
235 my ( $c, %keys ) = @_;
238 @{ $c->session->{__expire_keys} }{keys %keys} = map { $now + $_ } values %keys;
241 sub initialize_session_data {
246 return $c->_session({
249 __expires => $now + $c->config->{session}{expires},
252 $c->config->{session}{verify_address}
253 ? ( __address => $c->request->address )
259 sub generate_session_id {
262 my $digest = $c->_find_digest();
263 $digest->add( $c->session_hash_seed() );
264 return $digest->hexdigest;
267 sub create_session_id {
270 if ( !$c->_sessionid ) {
271 my $sid = $c->generate_session_id;
273 $c->log->debug(qq/Created session "$sid"/) if $c->debug;
281 sub session_hash_seed {
284 return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
289 sub _find_digest () {
291 foreach my $alg (qw/SHA-1 MD5 SHA-256/) {
293 my $obj = Digest->new($alg);
299 or Catalyst::Exception->throw(
300 "Could not find a suitable Digest module. Please install "
301 . "Digest::SHA1, Digest::SHA, or Digest::MD5" );
304 return Digest->new($usable);
311 $c->NEXT::dump_these(),
314 ? ( [ "Session ID" => $c->sessionid ], [ Session => $c->session ], )
327 Catalyst::Plugin::Session - Generic Session plugin - ties together server side
328 storage and client side state required to maintain session data.
332 # To get sessions to "just work", all you need to do is use these plugins:
336 Session::Store::FastMmap
337 Session::State::Cookie
340 # you can replace Store::FastMmap with Store::File - both have sensible
341 # default configurations (see their docs for details)
343 # more complicated backends are available for other scenarios (DBI storage,
347 # after you've loaded the plugins you can save session data
348 # For example, if you are writing a shopping cart, it could be implemented
351 sub add_item : Local {
352 my ( $self, $c ) = @_;
354 my $item_id = $c->req->param("item");
356 # $c->session is a hash ref, a bit like $c->stash
357 # the difference is that it' preserved across requests
359 push @{ $c->session->{items} }, $item_id;
361 $c->forward("MyView");
364 sub display_items : Local {
365 my ( $self, $c ) = @_;
367 # values in $c->session are restored
368 $c->stash->{items_to_display} =
369 [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
371 $c->forward("MyView");
376 The Session plugin is the base of two related parts of functionality required
377 for session management in web applications.
379 The first part, the State, is getting the browser to repeat back a session key,
380 so that the web application can identify the client and logically string
381 several requests together into a session.
383 The second part, the Store, deals with the actual storage of information about
384 the client. This data is stored so that the it may be revived for every request
385 made by the same client.
387 This plugin links the two pieces together.
389 =head1 RECCOMENDED BACKENDS
393 =item Session::State::Cookie
395 The only really sane way to do state is using cookies.
397 =item Session::Store::File
399 A portable backend, based on Cache::File.
401 =item Session::Store::FastMmap
403 A fast and flexible backend, based on Cache::FastMmap.
413 An accessor for the session ID value.
417 Returns a hash reference that might contain unserialized values from previous
418 requests in the same session, and whose modified value will be saved for future
421 This method will automatically create a new session and session ID if none
426 This is like Ruby on Rails' flash data structure. Think of it as a stash that
427 lasts a single redirect, not only a forward.
430 my ( $self, $c ) = @_;
432 $c->flash->{beans} = 10;
433 $c->response->redirect( $c->uri_for("foo") );
437 my ( $self, $c ) = @_;
439 my $value = $c->flash->{beans};
443 $c->response->redirect( $c->uri_for("bar") );
447 my ( $self, $c ) = @_;
449 if ( exists $c->flash->{beans} ) { # false
454 =item session_delete_reason
456 This accessor contains a string with the reason a session was deleted. Possible
471 =item session_expire_key $key, $ttl
473 Mark a key to expire at a certain time (only useful when shorter than the
474 expiry time for the whole session).
478 __PACKAGE__->config->{session}{expires} = 1000000000000; # forever
482 $c->session_expire_key( __user => 3600 );
484 Will make the session data survive, but the user will still be logged out after
487 Note that these values are not auto extended.
491 =item INTERNAL METHODS
497 This method is extended to also make calls to
498 C<check_session_plugin_requirements> and C<setup_session>.
500 =item check_session_plugin_requirements
502 This method ensures that a State and a Store plugin are also in use by the
507 This method populates C<< $c->config->{session} >> with the default values
508 listed in L</CONFIGURATION>.
512 This methoid is extended.
514 It's only effect is if the (off by default) C<flash_to_stash> configuration
515 parameter is on - then it will copy the contents of the flash to the stash at
520 This method is extended and will extend the expiry time, as well as persist the
521 session data if a session exists.
523 =item delete_session REASON
525 This method is used to invalidate a session. It takes an optional parameter
526 which will be saved in C<session_delete_reason> if provided.
528 =item initialize_session_data
530 This method will initialize the internal structure of the session, and is
531 called by the C<session> method if appropriate.
533 =item create_session_id
535 Creates a new session id using C<generate_session_id> if there is no session ID
538 =item generate_session_id
540 This method will return a string that can be used as a session ID. It is
541 supposed to be a reasonably random string with enough bits to prevent
542 collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
543 MD5 or SHA-256, depending on the availibility of these modules.
545 =item session_hash_seed
547 This method is actually rather internal to generate_session_id, but should be
548 overridable in case you want to provide more random data.
550 Currently it returns a concatenated string which contains:
552 =item validate_session_id SID
554 Make sure a session ID is of the right format.
556 This currently ensures that the session ID string is any amount of case
557 insensitive hexadecimal characters.
571 One value from C<rand>.
575 The stringified value of a newly allocated hash reference
579 The stringified value of the Catalyst context object
583 In the hopes that those combined values are entropic enough for most uses. If
584 this is not the case you can replace C<session_hash_seed> with e.g.
586 sub session_hash_seed {
587 open my $fh, "<", "/dev/random";
588 read $fh, my $bytes, 20;
593 Or even more directly, replace C<generate_session_id>:
595 sub generate_session_id {
596 open my $fh, "<", "/dev/random";
597 read $fh, my $bytes, 20;
599 return unpack("H*", $bytes);
602 Also have a look at L<Crypt::Random> and the various openssl bindings - these
603 modules provide APIs for cryptographically secure random data.
607 See L<Catalyst/dump_these> - ammends the session data structure to the list of
608 dumped objects if session ID is defined.
612 =head1 USING SESSIONS DURING PREPARE
614 The earliest point in time at which you may use the session data is after
615 L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
617 State plugins must set $c->session ID before C<prepare_action>, and during
618 C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
624 # don't touch $c->session yet!
626 $c->NEXT::prepare_action( @_ );
628 $c->session; # this is OK
629 $c->sessionid; # this is also OK
634 $c->config->{session} = {
638 All configuation parameters are provided in a hash reference under the
639 C<session> key in the configuration hash.
645 The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
650 When true, C<<$c->request->address>> will be checked at prepare time. If it is
651 not the same as the address that initiated the session, the session is deleted.
655 This option makes it easier to have actions behave the same whether they were
656 forwarded to or redirected to. On prepare time it copies the contents of
657 C<flash> (if any) to the stash.
663 The hash reference returned by C<< $c->session >> contains several keys which
664 are automatically set:
670 A timestamp whose value is the last second when the session is still valid. If
671 a session is restored, and __expires is less than the current time, the session
676 The last time a session was saved. This is the value of
677 C<< $c->session->{__expires} - $c->config->session->{expires} >>.
681 The time when the session was first created.
685 The value of C<< $c->request->address >> at the time the session was created.
686 This value is only populated if C<verify_address> is true in the configuration.
692 =head2 Round the Robin Proxies
694 C<verify_address> could make your site inaccessible to users who are behind
695 load balanced proxies. Some ISPs may give a different IP to each request by the
696 same client due to this type of proxying. If addresses are verified these
697 users' sessions cannot persist.
699 To let these users access your site you can either disable address verification
700 as a whole, or provide a checkbox in the login dialog that tells the server
701 that it's OK for the address of the client to change. When the server sees that
702 this box is checked it should delete the C<__address> sepcial key from the
703 session hash when the hash is first created.
705 =head2 Race Conditions
707 In this day and age where cleaning detergents and dutch football (not the
708 american kind) teams roam the plains in great numbers, requests may happen
709 simultaneously. This means that there is some risk of session data being
710 overwritten, like this:
716 request a starts, request b starts, with the same session id
720 session data is loaded in request a
724 session data is loaded in request b
728 session data is changed in request a
732 request a finishes, session data is updated and written to store
736 request b finishes, session data is updated and written to store, overwriting
741 If this is a concern in your application, a soon to be developed locking
742 solution is the only safe way to go. This will have a bigger overhead.
744 For applications where any given user is only making one request at a time this
745 plugin should be safe enough.
753 =item Christian Hansen
755 =item Yuval Kogman, C<nothingmuch@woobling.org> (current maintainer)
757 =item Sebastian Riedel
761 And countless other contributers from #catalyst. Thanks guys!
763 =head1 COPYRIGHT & LICENSE
765 Copyright (c) 2005 the aforementioned authors. All rights
766 reserved. This program is free software; you can redistribute
767 it and/or modify it under the same terms as Perl itself.