Better documentation of when exactly session related things happen
[catagits/Catalyst-Plugin-Session.git] / lib / Catalyst / Plugin / Session.pm
1 #!/usr/bin/perl
2
3 package Catalyst::Plugin::Session;
4 use base qw/Class::Accessor::Fast/;
5
6 use strict;
7 use warnings;
8
9 use NEXT;
10 use Catalyst::Exception ();
11 use Digest              ();
12 use overload            ();
13
14 our $VERSION = "0.01";
15
16 BEGIN {
17     __PACKAGE__->mk_accessors(qw/sessionid session_delete_reason/);
18 }
19
20 sub setup {
21     my $c = shift;
22
23     $c->NEXT::setup(@_);
24
25     $c->check_session_plugin_requirements;
26     $c->setup_session;
27
28     return $c;
29 }
30
31 sub check_session_plugin_requirements {
32     my $c = shift;
33
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." );
40
41         $c->log->fatal($err);
42         Catalyst::Exception->throw($err);
43     }
44 }
45
46 sub setup_session {
47     my $c = shift;
48
49     my $cfg = ( $c->config->{session} ||= {} );
50
51     %$cfg = (
52         expires        => 7200,
53         verify_address => 1,
54         %$cfg,
55     );
56
57     $c->NEXT::setup_session();
58 }
59
60 sub finalize {
61     my $c = shift;
62
63     if ( $c->{session} ) {
64
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(@_);
73 }
74
75 sub prepare_action {
76     my $c = shift;
77
78     if ( my $sid = $c->sessionid ) {
79         $c->log->debug(qq/Found session "$sid"/) if $c->debug;
80
81         my $s = $c->{session} ||= $c->get_session_data($sid);
82         if ( !$s or $s->{__expires} < time ) {
83
84             # session expired
85             $c->log->debug("Deleting session $sid (expired)") if $c->debug;
86             $c->delete_session("session expired");
87         }
88
89         elsif (   $c->config->{session}{verify_address}
90             && $c->{session}{__address}
91             && $c->{session}{__address} ne $c->request->address )
92         {
93             $c->log->warn(
94                     "Deleting session $sid due to address mismatch ("
95                   . $c->{session}{__address} . " != "
96                   . $c->request->address . ")",
97             );
98             $c->delete_session("address mismatch");
99         }
100     }
101
102     $c->NEXT::prepare_action(@_);
103 }
104
105 sub delete_session {
106     my ( $c, $msg ) = @_;
107
108     # delete the session data
109     my $sid = $c->sessionid;
110     $c->delete_session_data($sid);
111
112     # reset the values in the context object
113     $c->{session} = undef;
114     $c->sessionid(undef);
115     $c->session_delete_reason($msg);
116 }
117
118 sub session {
119     my $c = shift;
120
121     return $c->{session} if $c->{session};
122
123     my $sid = $c->generate_session_id;
124     $c->sessionid($sid);
125
126     $c->log->debug(qq/Created session "$sid"/) if $c->debug;
127
128     return $c->initialize_session_data;
129 }
130
131 sub initialize_session_data {
132     my $c = shift;
133
134     my $now = time;
135
136     return $c->{session} = {
137         __created => $now,
138         __updated => $now,
139         __expires => $now + $c->config->{session}{expires},
140
141         (
142             $c->config->{session}{verify_address}
143             ? ( __address => $c->request->address )
144             : ()
145         ),
146     };
147 }
148
149 sub 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
157 my $counter;
158
159 sub session_hash_seed {
160     my $c = shift;
161
162     return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
163 }
164
165 my $usable;
166
167 sub _find_digest () {
168     unless ($usable) {
169         foreach my $alg (qw/SHA-1 MD5 SHA-256/) {
170             eval {
171                                 my $obj = Digest->new($alg);
172                                 $usable = $alg;
173                                 return $obj;
174                         }
175         }
176         $usable
177           or Catalyst::Exception->throw(
178                 "Could not find a suitable Digest module. Please install "
179               . "Digest::SHA1, Digest::SHA, or Digest::MD5" );
180     }
181
182     return Digest->new($usable);
183 }
184
185 sub 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
197 __PACKAGE__;
198
199 __END__
200
201 =pod
202
203 =head1 NAME
204
205 Catalyst::Plugin::Session - Generic Session plugin - ties together server side
206 storage and client side tickets required to maintain session data.
207
208 =head1 SYNOPSIS
209
210     use Catalyst qw/Session Session::Store::FastMmap Session::State::Cookie/;
211
212     sub add_item : Local {
213         my ( $self, $c ) = @_;
214
215         my $item_id = $c->req->param("item");
216
217         # $c->session is stored across requests, so
218         # other actions will see these values
219
220         push @{ $c->session->{items} }, $item_id;
221
222         $c->forward("MyView");
223     }
224
225     sub display_items : Local {
226         my ( $self, $c ) = @_;
227
228         # values in $c->session are restored
229         $c->stash->{items_to_display} =
230             [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
231
232         $c->forward("MyView");
233     }
234
235 =head1 DESCRIPTION
236
237 The Session plugin is the base of two related parts of functionality required
238 for session management in web applications.
239
240 The first part, the State, is getting the browser to repeat back a session key,
241 so that the web application can identify the client and logically string
242 several requests together into a session.
243
244 The second part, the Store, deals with the actual storage of information about
245 the client. This data is stored so that the it may be revived for every request
246 made by the same client.
247
248 This plugin links the two pieces together.
249
250 =head1 METHODS
251
252 =over 4
253
254 =item sessionid
255
256 An accessor for the session ID value.
257
258 =item session
259
260 Returns a hash reference that might contain unserialized values from previous
261 requests in the same session, and whose modified value will be saved for future
262 requests.
263
264 This method will automatically create a new session and session ID if none
265 exists.
266
267 =item session_delete_reason
268
269 This accessor contains a string with the reason a session was deleted. Possible
270 values include:
271
272 =over 4
273
274 =item *
275
276 C<address mismatch>
277
278 =item *
279
280 C<session expired>
281
282 =back
283
284 =item setup
285
286 This method is extended to also make calls to
287 C<check_session_plugin_requirements> and C<setup_session>.
288
289 =item check_session_plugin_requirements
290
291 This method ensures that a State and a Store plugin are also in use by the
292 application.
293
294 =item setup_session
295
296 This method populates C<< $c->config->{session} >> with the default values
297 listed in L</CONFIGURATION>.
298
299 =item prepare_action
300
301 This methoid is extended, and will restore session data and check it for
302 validity if a session id is defined. It assumes that the State plugin will
303 populate the C<sessionid> key beforehand.
304
305 =item finalize
306
307 This method is extended and will extend the expiry time, as well as persist the
308 session data if a session exists.
309
310 =item delete_session REASON
311
312 This method is used to invalidate a session. It takes an optional parameter
313 which will be saved in C<session_delete_reason> if provided.
314
315 =item initialize_session_data
316
317 This method will initialize the internal structure of the session, and is
318 called by the C<session> method if appropriate.
319
320 =item generate_session_id
321
322 This method will return a string that can be used as a session ID. It is
323 supposed to be a reasonably random string with enough bits to prevent
324 collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
325 MD5 or SHA-256, depending on the availibility of these modules.
326
327 =item session_hash_seed
328
329 This method is actually rather internal to generate_session_id, but should be
330 overridable in case you want to provide more random data.
331
332 Currently it returns a concatenated string which contains:
333
334 =over 4
335
336 =item *
337
338 A counter
339
340 =item *
341
342 The current time
343
344 =item *
345
346 One value from C<rand>.
347
348 =item *
349
350 The stringified value of a newly allocated hash reference
351
352 =item *
353
354 The stringified value of the Catalyst context object
355
356 =back
357
358 In the hopes that those combined values are entropic enough for most uses. If
359 this is not the case you can replace C<session_hash_seed> with e.g.
360
361     sub session_hash_seed {
362         open my $fh, "<", "/dev/random";
363         read $fh, my $bytes, 20;
364         close $fh;
365         return $bytes;
366     }
367
368 Or even more directly, replace C<generate_session_id>:
369
370     sub generate_session_id {
371         open my $fh, "<", "/dev/random";
372         read $fh, my $bytes, 20;
373         close $fh;
374         return unpack("H*", $bytes);
375     }
376
377 Also have a look at L<Crypt::Random> and the various openssl bindings - these
378 modules provide APIs for cryptographically secure random data.
379
380 =item dump_these
381
382 See L<Catalyst/dump_these> - ammends the session data structure to the list of
383 dumped objects if session ID is defined.
384
385 =back
386
387 =head1 USING SESSIONS DURING PREPARE
388
389 The earliest point in time at which you may use the session data is after
390 L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
391
392 State plugins must set $c->session ID before C<prepare_action>, and during
393 C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
394 the store.
395
396         sub prepare_action {
397                 my $c = shift;
398
399                 # don't touch $c->session yet!
400                 
401                 $c->NEXT::prepare_action( @_ );
402
403                 $c->session;  # this is OK
404                 $c->sessionid; # this is also OK
405         }
406
407 =head1 CONFIGURATION
408
409     $c->config->{session} = {
410         expires => 1234,
411     };
412
413 All configuation parameters are provided in a hash reference under the
414 C<session> key in the configuration hash.
415
416 =over 4
417
418 =item expires
419
420 The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
421 hours).
422
423 =item verify_address
424
425 When false, C<< $c->request->address >> will be checked at prepare time. If it
426 is not the same as the address that initiated the session, the session is
427 deleted.
428
429 =back
430
431 =head1 SPECIAL KEYS
432
433 The hash reference returned by C<< $c->session >> contains several keys which
434 are automatically set:
435
436 =over 4
437
438 =item __expires
439
440 A timestamp whose value is the last second when the session is still valid. If
441 a session is restored, and __expires is less than the current time, the session
442 is deleted.
443
444 =item __updated
445
446 The last time a session was saved. This is the value of
447 C<< $c->{session}{__expires} - $c->config->{session}{expires} >>.
448
449 =item __created
450
451 The time when the session was first created.
452
453 =item __address
454
455 The value of C<< $c->request->address >> at the time the session was created.
456 This value is only populated of C<verify_address> is true in the configuration.
457
458 =back
459
460 =head1 CAVEATS
461
462 C<verify_address> could make your site inaccessible to users who are behind
463 load balanced proxies. Some ISPs may give a different IP to each request by the
464 same client due to this type of proxying. If addresses are verified these
465 users' sessions cannot persist.
466
467 To let these users access your site you can either disable address verification
468 as a whole, or provide a checkbox in the login dialog that tells the server
469 that it's OK for the address of the client to change. When the server sees that
470 this box is checked it should delete the C<__address> sepcial key from the
471 session hash when the hash is first created.
472
473 =cut
474
475