Doc improvements 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
37160715 14our $VERSION = "0.01";
15
9e447f9d 16BEGIN {
9a9252c2 17 __PACKAGE__->mk_accessors(qw/sessionid 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
9a9252c2 63 if ( $c->{session} ) {
9e447f9d 64
9a9252c2 65 # all sessions are extended at the end of the request
66 my $now = time;
67 @{ $c->{session} }{qw/__updated __expires/} =
68 ( $now, $c->config->{session}{expires} + $now );
69 $c->store_session_data( $c->sessionid, $c->{session} );
70 }
71
72 $c->NEXT::finalize(@_);
9e447f9d 73}
74
75sub prepare_action {
76 my $c = shift;
77
3f182468 78 if ( my $sid = $c->sessionid ) {
3f182468 79 my $s = $c->{session} ||= $c->get_session_data($sid);
80 if ( !$s or $s->{__expires} < time ) {
81
82 # session expired
83 $c->log->debug("Deleting session $sid (expired)") if $c->debug;
84 $c->delete_session("session expired");
85 }
29543a62 86 elsif ($c->config->{session}{verify_address}
3f182468 87 && $c->{session}{__address}
88 && $c->{session}{__address} ne $c->request->address )
89 {
90 $c->log->warn(
91 "Deleting session $sid due to address mismatch ("
92 . $c->{session}{__address} . " != "
93 . $c->request->address . ")",
94 );
95 $c->delete_session("address mismatch");
96 }
29543a62 97 else {
98 $c->log->debug(qq/Restored session "$sid"/) if $c->debug;
99 }
9a9252c2 100 }
101
3f182468 102 $c->NEXT::prepare_action(@_);
9e447f9d 103}
104
105sub delete_session {
9a9252c2 106 my ( $c, $msg ) = @_;
9e447f9d 107
9a9252c2 108 # delete the session data
109 my $sid = $c->sessionid;
110 $c->delete_session_data($sid);
9e447f9d 111
9a9252c2 112 # reset the values in the context object
113 $c->{session} = undef;
114 $c->sessionid(undef);
115 $c->session_delete_reason($msg);
9e447f9d 116}
117
118sub session {
9a9252c2 119 my $c = shift;
9e447f9d 120
121 return $c->{session} if $c->{session};
122
9a9252c2 123 my $sid = $c->generate_session_id;
124 $c->sessionid($sid);
9e447f9d 125
9a9252c2 126 $c->log->debug(qq/Created session "$sid"/) if $c->debug;
9e447f9d 127
9a9252c2 128 return $c->initialize_session_data;
9e447f9d 129}
130
131sub initialize_session_data {
9a9252c2 132 my $c = shift;
9e447f9d 133
9a9252c2 134 my $now = time;
9e447f9d 135
9a9252c2 136 return $c->{session} = {
137 __created => $now,
138 __updated => $now,
139 __expires => $now + $c->config->{session}{expires},
9e447f9d 140
9a9252c2 141 (
142 $c->config->{session}{verify_address}
143 ? ( __address => $c->request->address )
144 : ()
145 ),
146 };
9e447f9d 147}
148
9e447f9d 149sub generate_session_id {
150 my $c = shift;
151
152 my $digest = $c->_find_digest();
153 $digest->add( $c->session_hash_seed() );
154 return $digest->hexdigest;
155}
156
157my $counter;
9a9252c2 158
9e447f9d 159sub session_hash_seed {
9a9252c2 160 my $c = shift;
161
162 return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
9e447f9d 163}
164
165my $usable;
9a9252c2 166
9e447f9d 167sub _find_digest () {
9a9252c2 168 unless ($usable) {
7d139eeb 169 foreach my $alg (qw/SHA-1 MD5 SHA-256/) {
170 eval {
29543a62 171 my $obj = Digest->new($alg);
172 $usable = $alg;
173 return $obj;
174 };
7d139eeb 175 }
176 $usable
9a9252c2 177 or Catalyst::Exception->throw(
178 "Could not find a suitable Digest module. Please install "
179 . "Digest::SHA1, Digest::SHA, or Digest::MD5" );
180 }
9e447f9d 181
182 return Digest->new($usable);
183}
184
99b2191e 185sub dump_these {
186 my $c = shift;
187
188 (
189 $c->NEXT::dump_these(),
190
191 $c->sessionid
192 ? ( [ "Session ID" => $c->sessionid ], [ Session => $c->session ], )
193 : ()
194 );
195}
196
9e447f9d 197__PACKAGE__;
198
199__END__
200
201=pod
202
203=head1 NAME
204
205Catalyst::Plugin::Session - Generic Session plugin - ties together server side
206storage and client side tickets required to maintain session data.
207
208=head1 SYNOPSIS
209
8f0b4c16 210 # To get sessions to "just work", all you need to do is use these plugins:
211
212 use Catalyst qw/
213 Session
214 Session::Store::FastMmap
215 Session::State::Cookie
216 /;
217
218 # you can replace Store::FastMmap with Store::File - both have sensible
219 # default configurations (see their docs for details)
220
221 # more complicated backends are available for other scenarios (DBI storage,
222 # etc)
223
224
225 # after you've loaded the plugins you can save session data
226 # For example, if you are writing a shopping cart, it could be implemented
227 # like this:
9e447f9d 228
229a5b53 229 sub add_item : Local {
230 my ( $self, $c ) = @_;
231
232 my $item_id = $c->req->param("item");
233
8f0b4c16 234 # $c->session is a hash ref, a bit like $c->stash
235 # the difference is that it' preserved across requests
229a5b53 236
237 push @{ $c->session->{items} }, $item_id;
238
239 $c->forward("MyView");
240 }
241
242 sub display_items : Local {
243 my ( $self, $c ) = @_;
244
245 # values in $c->session are restored
246 $c->stash->{items_to_display} =
8f0b4c16 247 [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
229a5b53 248
249 $c->forward("MyView");
250 }
251
9e447f9d 252=head1 DESCRIPTION
253
254The Session plugin is the base of two related parts of functionality required
255for session management in web applications.
256
257The first part, the State, is getting the browser to repeat back a session key,
258so that the web application can identify the client and logically string
259several requests together into a session.
260
261The second part, the Store, deals with the actual storage of information about
262the client. This data is stored so that the it may be revived for every request
263made by the same client.
264
265This plugin links the two pieces together.
266
8f0b4c16 267=head1 RECCOMENDED BACKENDS
268
269=over 4
270
271=item Session::State::Cookie
272
273The only really sane way to do state is using cookies.
274
275=item Session::Store::File
276
277A portable backend, based on Cache::File.
278
279=item Session::Store::FastMmap
280
281A fast and flexible backend, based on Cache::FastMmap.
282
283=back
284
9e447f9d 285=head1 METHODS
286
287=over 4
288
289=item sessionid
290
291An accessor for the session ID value.
292
293=item session
294
295Returns a hash reference that might contain unserialized values from previous
296requests in the same session, and whose modified value will be saved for future
297requests.
298
299This method will automatically create a new session and session ID if none
300exists.
301
302=item session_delete_reason
303
304This accessor contains a string with the reason a session was deleted. Possible
305values include:
306
307=over 4
308
309=item *
310
311C<address mismatch>
312
313=item *
314
315C<session expired>
316
317=back
318
8f0b4c16 319=back
320
321=item INTERNAL METHODS
322
323=over 4
324
9e447f9d 325=item setup
326
327This method is extended to also make calls to
328C<check_session_plugin_requirements> and C<setup_session>.
329
330=item check_session_plugin_requirements
331
332This method ensures that a State and a Store plugin are also in use by the
333application.
334
335=item setup_session
336
337This method populates C<< $c->config->{session} >> with the default values
338listed in L</CONFIGURATION>.
339
340=item prepare_action
341
342This methoid is extended, and will restore session data and check it for
343validity if a session id is defined. It assumes that the State plugin will
344populate the C<sessionid> key beforehand.
345
346=item finalize
347
348This method is extended and will extend the expiry time, as well as persist the
349session data if a session exists.
350
351=item delete_session REASON
352
353This method is used to invalidate a session. It takes an optional parameter
354which will be saved in C<session_delete_reason> if provided.
355
356=item initialize_session_data
357
358This method will initialize the internal structure of the session, and is
359called by the C<session> method if appropriate.
360
229a5b53 361=item generate_session_id
362
363This method will return a string that can be used as a session ID. It is
364supposed to be a reasonably random string with enough bits to prevent
365collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
366MD5 or SHA-256, depending on the availibility of these modules.
367
368=item session_hash_seed
369
370This method is actually rather internal to generate_session_id, but should be
371overridable in case you want to provide more random data.
372
373Currently it returns a concatenated string which contains:
374
375=over 4
376
377=item *
378
379A counter
380
381=item *
382
383The current time
384
385=item *
386
387One value from C<rand>.
388
389=item *
390
391The stringified value of a newly allocated hash reference
392
393=item *
394
395The stringified value of the Catalyst context object
396
397=back
398
399In the hopes that those combined values are entropic enough for most uses. If
400this is not the case you can replace C<session_hash_seed> with e.g.
401
402 sub session_hash_seed {
403 open my $fh, "<", "/dev/random";
404 read $fh, my $bytes, 20;
405 close $fh;
406 return $bytes;
407 }
408
409Or even more directly, replace C<generate_session_id>:
410
411 sub generate_session_id {
412 open my $fh, "<", "/dev/random";
413 read $fh, my $bytes, 20;
414 close $fh;
415 return unpack("H*", $bytes);
416 }
417
418Also have a look at L<Crypt::Random> and the various openssl bindings - these
419modules provide APIs for cryptographically secure random data.
420
99b2191e 421=item dump_these
422
423See L<Catalyst/dump_these> - ammends the session data structure to the list of
424dumped objects if session ID is defined.
425
9e447f9d 426=back
427
a92c8aeb 428=head1 USING SESSIONS DURING PREPARE
429
430The earliest point in time at which you may use the session data is after
431L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
432
433State plugins must set $c->session ID before C<prepare_action>, and during
434C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
435the store.
436
437 sub prepare_action {
438 my $c = shift;
439
440 # don't touch $c->session yet!
441
442 $c->NEXT::prepare_action( @_ );
443
444 $c->session; # this is OK
445 $c->sessionid; # this is also OK
446 }
447
9e447f9d 448=head1 CONFIGURATION
449
229a5b53 450 $c->config->{session} = {
451 expires => 1234,
452 };
9e447f9d 453
454All configuation parameters are provided in a hash reference under the
455C<session> key in the configuration hash.
456
457=over 4
458
459=item expires
460
461The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
462hours).
463
464=item verify_address
465
466When false, C<< $c->request->address >> will be checked at prepare time. If it
467is not the same as the address that initiated the session, the session is
468deleted.
469
470=back
471
472=head1 SPECIAL KEYS
473
474The hash reference returned by C<< $c->session >> contains several keys which
475are automatically set:
476
477=over 4
478
479=item __expires
480
481A timestamp whose value is the last second when the session is still valid. If
482a session is restored, and __expires is less than the current time, the session
483is deleted.
484
485=item __updated
486
487The last time a session was saved. This is the value of
488C<< $c->{session}{__expires} - $c->config->{session}{expires} >>.
489
490=item __created
491
492The time when the session was first created.
493
494=item __address
495
496The value of C<< $c->request->address >> at the time the session was created.
497This value is only populated of C<verify_address> is true in the configuration.
498
499=back
500
c80e9f04 501=head1 CAVEATS
502
503C<verify_address> could make your site inaccessible to users who are behind
504load balanced proxies. Some ISPs may give a different IP to each request by the
505same client due to this type of proxying. If addresses are verified these
506users' sessions cannot persist.
507
508To let these users access your site you can either disable address verification
509as a whole, or provide a checkbox in the login dialog that tells the server
510that it's OK for the address of the client to change. When the server sees that
511this box is checked it should delete the C<__address> sepcial key from the
512session hash when the hash is first created.
513
d45028d6 514=head1 AUTHORS
515
36316211 516Andy Grundman
517Christian Hansen
518Yuval Kogman, C<nothingmuch@woobling.org>
519Sebastian Riedel
d45028d6 520
521=head1 COPYRIGHT & LICNESE
522
523 Copyright (c) 2005 the aforementioned authors. All rights
524 reserved. This program is free software; you can redistribute
525 it and/or modify it under the same terms as Perl itself.
526
9e447f9d 527=cut
528
529