Commit | Line | Data |
9e447f9d |
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 (); |
9a9252c2 |
11 | use Digest (); |
12 | use overload (); |
13 | use List::Util (); |
9e447f9d |
14 | |
15 | BEGIN { |
9a9252c2 |
16 | __PACKAGE__->mk_accessors(qw/sessionid session_delete_reason/); |
9e447f9d |
17 | } |
18 | |
19 | sub setup { |
9a9252c2 |
20 | my $c = shift; |
21 | |
22 | $c->NEXT::setup(@_); |
23 | |
24 | $c->check_session_plugin_requirements; |
25 | $c->setup_session; |
26 | |
27 | return $c; |
9e447f9d |
28 | } |
29 | |
30 | sub check_session_plugin_requirements { |
9a9252c2 |
31 | my $c = shift; |
9e447f9d |
32 | |
9a9252c2 |
33 | unless ( $c->isa("Catalyst::Plugin::Session::State") |
34 | && $c->isa("Catalyst::Plugin::Session::Store") ) |
35 | { |
36 | my $err = |
37 | ( "The Session plugin requires both Session::State " |
38 | . "and Session::Store plugins to be used as well." ); |
9e447f9d |
39 | |
9a9252c2 |
40 | $c->log->fatal($err); |
41 | Catalyst::Exception->throw($err); |
42 | } |
9e447f9d |
43 | } |
44 | |
45 | sub setup_session { |
9a9252c2 |
46 | my $c = shift; |
9e447f9d |
47 | |
9a9252c2 |
48 | my $cfg = ( $c->config->{session} ||= {} ); |
9e447f9d |
49 | |
9a9252c2 |
50 | %$cfg = ( |
51 | expires => 7200, |
52 | verify_address => 1, |
53 | %$cfg, |
54 | ); |
9e447f9d |
55 | |
9a9252c2 |
56 | $c->NEXT::setup_session(); |
9e447f9d |
57 | } |
58 | |
59 | sub finalize { |
9a9252c2 |
60 | my $c = shift; |
9e447f9d |
61 | |
9a9252c2 |
62 | if ( $c->{session} ) { |
9e447f9d |
63 | |
9a9252c2 |
64 | # all sessions are extended at the end of the request |
65 | my $now = time; |
66 | @{ $c->{session} }{qw/__updated __expires/} = |
67 | ( $now, $c->config->{session}{expires} + $now ); |
68 | $c->store_session_data( $c->sessionid, $c->{session} ); |
69 | } |
70 | |
71 | $c->NEXT::finalize(@_); |
9e447f9d |
72 | } |
73 | |
74 | sub prepare_action { |
75 | my $c = shift; |
76 | |
9a9252c2 |
77 | my $ret = $c->NEXT::prepare_action; |
9e447f9d |
78 | |
9a9252c2 |
79 | my $sid = $c->sessionid || return; |
9e447f9d |
80 | |
81 | $c->log->debug(qq/Found session "$sid"/) if $c->debug; |
82 | |
9a9252c2 |
83 | my $s = $c->{session} ||= $c->get_session_data($sid); |
84 | if ( !$s or $s->{__expires} < time ) { |
85 | |
86 | # session expired |
87 | $c->log->debug("Deleting session $sid (expired)") if $c->debug; |
88 | $c->delete_session("session expired"); |
89 | return $ret; |
90 | } |
91 | |
92 | if ( $c->config->{session}{verify_address} |
93 | && $c->{session}{__address} |
94 | && $c->{session}{__address} ne $c->request->address ) |
95 | { |
96 | $c->log->warn( |
97 | "Deleting session $sid due to address mismatch (" |
98 | . $c->{session}{__address} . " != " |
99 | . $c->request->address . ")", |
100 | ); |
101 | $c->delete_session("address mismatch"); |
102 | return $ret; |
103 | } |
9e447f9d |
104 | } |
105 | |
106 | sub 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 |
114 | $c->{session} = undef; |
115 | $c->sessionid(undef); |
116 | $c->session_delete_reason($msg); |
9e447f9d |
117 | } |
118 | |
119 | sub session { |
9a9252c2 |
120 | my $c = shift; |
9e447f9d |
121 | |
122 | return $c->{session} if $c->{session}; |
123 | |
9a9252c2 |
124 | my $sid = $c->generate_session_id; |
125 | $c->sessionid($sid); |
9e447f9d |
126 | |
9a9252c2 |
127 | $c->log->debug(qq/Created session "$sid"/) if $c->debug; |
9e447f9d |
128 | |
9a9252c2 |
129 | return $c->initialize_session_data; |
9e447f9d |
130 | } |
131 | |
132 | sub initialize_session_data { |
9a9252c2 |
133 | my $c = shift; |
9e447f9d |
134 | |
9a9252c2 |
135 | my $now = time; |
9e447f9d |
136 | |
9a9252c2 |
137 | return $c->{session} = { |
138 | __created => $now, |
139 | __updated => $now, |
140 | __expires => $now + $c->config->{session}{expires}, |
9e447f9d |
141 | |
9a9252c2 |
142 | ( |
143 | $c->config->{session}{verify_address} |
144 | ? ( __address => $c->request->address ) |
145 | : () |
146 | ), |
147 | }; |
9e447f9d |
148 | } |
149 | |
9e447f9d |
150 | # refactor into Catalyst::Plugin::Session::ID::Weak ? |
151 | |
152 | sub generate_session_id { |
153 | my $c = shift; |
154 | |
155 | my $digest = $c->_find_digest(); |
156 | $digest->add( $c->session_hash_seed() ); |
157 | return $digest->hexdigest; |
158 | } |
159 | |
160 | my $counter; |
9a9252c2 |
161 | |
9e447f9d |
162 | sub session_hash_seed { |
9a9252c2 |
163 | my $c = shift; |
164 | |
165 | return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), ); |
9e447f9d |
166 | } |
167 | |
168 | my $usable; |
9a9252c2 |
169 | |
9e447f9d |
170 | sub _find_digest () { |
9a9252c2 |
171 | unless ($usable) { |
172 | $usable = List::Util::first( |
173 | sub { |
174 | eval { Digest->new($_) }; |
175 | }, |
176 | qw/SHA-1 MD5 SHA-256/ |
177 | ) |
178 | or Catalyst::Exception->throw( |
179 | "Could not find a suitable Digest module. Please install " |
180 | . "Digest::SHA1, Digest::SHA, or Digest::MD5" ); |
181 | } |
9e447f9d |
182 | |
183 | return Digest->new($usable); |
184 | } |
185 | |
9e447f9d |
186 | __PACKAGE__; |
187 | |
188 | __END__ |
189 | |
190 | =pod |
191 | |
192 | =head1 NAME |
193 | |
194 | Catalyst::Plugin::Session - Generic Session plugin - ties together server side |
195 | storage and client side tickets required to maintain session data. |
196 | |
197 | =head1 SYNOPSIS |
198 | |
199 | use Catalyst qw/Session Session::Store::FastMmap Session::State::Cookie/; |
200 | |
201 | =head1 DESCRIPTION |
202 | |
203 | The Session plugin is the base of two related parts of functionality required |
204 | for session management in web applications. |
205 | |
206 | The first part, the State, is getting the browser to repeat back a session key, |
207 | so that the web application can identify the client and logically string |
208 | several requests together into a session. |
209 | |
210 | The second part, the Store, deals with the actual storage of information about |
211 | the client. This data is stored so that the it may be revived for every request |
212 | made by the same client. |
213 | |
214 | This plugin links the two pieces together. |
215 | |
216 | =head1 METHODS |
217 | |
218 | =over 4 |
219 | |
220 | =item sessionid |
221 | |
222 | An accessor for the session ID value. |
223 | |
224 | =item session |
225 | |
226 | Returns a hash reference that might contain unserialized values from previous |
227 | requests in the same session, and whose modified value will be saved for future |
228 | requests. |
229 | |
230 | This method will automatically create a new session and session ID if none |
231 | exists. |
232 | |
233 | =item session_delete_reason |
234 | |
235 | This accessor contains a string with the reason a session was deleted. Possible |
236 | values include: |
237 | |
238 | =over 4 |
239 | |
240 | =item * |
241 | |
242 | C<address mismatch> |
243 | |
244 | =item * |
245 | |
246 | C<session expired> |
247 | |
248 | =back |
249 | |
250 | =item setup |
251 | |
252 | This method is extended to also make calls to |
253 | C<check_session_plugin_requirements> and C<setup_session>. |
254 | |
255 | =item check_session_plugin_requirements |
256 | |
257 | This method ensures that a State and a Store plugin are also in use by the |
258 | application. |
259 | |
260 | =item setup_session |
261 | |
262 | This method populates C<< $c->config->{session} >> with the default values |
263 | listed in L</CONFIGURATION>. |
264 | |
265 | =item prepare_action |
266 | |
267 | This methoid is extended, and will restore session data and check it for |
268 | validity if a session id is defined. It assumes that the State plugin will |
269 | populate the C<sessionid> key beforehand. |
270 | |
271 | =item finalize |
272 | |
273 | This method is extended and will extend the expiry time, as well as persist the |
274 | session data if a session exists. |
275 | |
276 | =item delete_session REASON |
277 | |
278 | This method is used to invalidate a session. It takes an optional parameter |
279 | which will be saved in C<session_delete_reason> if provided. |
280 | |
281 | =item initialize_session_data |
282 | |
283 | This method will initialize the internal structure of the session, and is |
284 | called by the C<session> method if appropriate. |
285 | |
286 | =back |
287 | |
288 | =head1 CONFIGURATION |
289 | |
290 | $c->config->{session} = { |
291 | expires => 1234, |
292 | }; |
293 | |
294 | All configuation parameters are provided in a hash reference under the |
295 | C<session> key in the configuration hash. |
296 | |
297 | =over 4 |
298 | |
299 | =item expires |
300 | |
301 | The time-to-live of each session, expressed in seconds. Defaults to 7200 (two |
302 | hours). |
303 | |
304 | =item verify_address |
305 | |
306 | When false, C<< $c->request->address >> will be checked at prepare time. If it |
307 | is not the same as the address that initiated the session, the session is |
308 | deleted. |
309 | |
310 | =back |
311 | |
312 | =head1 SPECIAL KEYS |
313 | |
314 | The hash reference returned by C<< $c->session >> contains several keys which |
315 | are automatically set: |
316 | |
317 | =over 4 |
318 | |
319 | =item __expires |
320 | |
321 | A timestamp whose value is the last second when the session is still valid. If |
322 | a session is restored, and __expires is less than the current time, the session |
323 | is deleted. |
324 | |
325 | =item __updated |
326 | |
327 | The last time a session was saved. This is the value of |
328 | C<< $c->{session}{__expires} - $c->config->{session}{expires} >>. |
329 | |
330 | =item __created |
331 | |
332 | The time when the session was first created. |
333 | |
334 | =item __address |
335 | |
336 | The value of C<< $c->request->address >> at the time the session was created. |
337 | This value is only populated of C<verify_address> is true in the configuration. |
338 | |
339 | =back |
340 | |
341 | =cut |
342 | |
343 | |