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