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