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