File Coverage

blib/lib/Catalyst/Plugin/Session/State/URI.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package Catalyst::Plugin::Session::State::URI;
2              
3 4     4   193772 use Moose;
  0            
  0            
4             use HTML::TokeParser::Simple;
5             use MIME::Types;
6             use MRO::Compat;
7             use URI;
8             use URI::Find;
9             use URI::QueryParam;
10              
11             use namespace::clean -except => 'meta';
12              
13             our $VERSION = '0.15';
14              
15             extends 'Catalyst::Plugin::Session::State';
16             with 'MooseX::Emulate::Class::Accessor::Fast';
17              
18             __PACKAGE__->mk_accessors(qw/_sessionid_from_uri _sessionid_to_rewrite/);
19              
20             sub get_session_id {
21             my ( $c, @args ) = @_;
22             return $c->_sessionid_from_uri || $c->maybe::next::method(@args);
23             }
24              
25             sub set_session_id {
26             my ( $c, $sid, @args ) = @_;
27             $c->_sessionid_to_rewrite($sid);
28             $c->maybe::next::method($sid, @args);
29             }
30              
31             sub delete_session_id {
32             my ( $c, @args ) = @_;
33             $c->_sessionid_from_uri(undef);
34             $c->_sessionid_to_rewrite(undef);
35             $c->maybe::next::method(@args);
36             }
37              
38             sub setup_session {
39             my $c = shift();
40              
41             $c->maybe::next::method(@_);
42              
43             my %defaults = (
44             rewrite_redirect => 1,
45             rewrite_body => 1,
46             no_rewrite_if_cookie => 1,
47             );
48              
49             my $config = $c->_session_plugin_config;
50              
51              
52             if ( delete $config->{rewrite} ) {
53             $config->{rewrite_redirect} = 1
54             unless exists $config->{rewrite_redirect};
55             $config->{rewrite_body} = 1
56             unless exists $config->{rewrite_body};
57             }
58              
59             foreach my $key ( keys %defaults ) {
60             $config->{$key} = $defaults{$key}
61             unless exists $config->{$key};
62             }
63             }
64              
65             sub finalize {
66             my $c = shift;
67              
68             $c->session_rewrite_if_needed;
69              
70             return $c->maybe::next::method(@_);
71             }
72              
73              
74             sub session_rewrite_if_needed {
75             my $c = shift;
76              
77             my $sid = $c->_sessionid_to_rewrite || $c->_sessionid_from_uri;
78              
79             if ( $sid and $c->session_should_rewrite ) {
80             $c->log->debug("rewriting response elements to include session id")
81             if $c->debug;
82              
83             if ( $c->session_should_rewrite_redirect ) {
84             $c->rewrite_redirect_with_session_id($sid);
85             }
86              
87             if ( $c->session_should_rewrite_body ) {
88             $c->rewrite_body_with_session_id($sid);
89             }
90             }
91             }
92              
93             sub rewrite_body_with_session_id {
94             my ( $c, $sid ) = @_;
95              
96             if (
97             ($c->response->content_type || '') =~ /html/ # XML too?
98             or
99             (!$c->response->content_type and $c->response->body =~ /^\s*\w*\s*<[?!]?\s*\w+/ ), # if it looks like html
100             ) {
101             $c->rewrite_html_with_session_id($sid);
102             } else {
103             $c->rewrite_text_with_session_id($sid);
104             }
105              
106             }
107              
108             sub _session_rewriting_html_tag_map {
109             return {
110             a => "href",
111             form => "action",
112             link => "href",
113             img => "src",
114             script => "src",
115             };
116             }
117              
118             sub rewrite_html_with_session_id {
119             my ( $c, $sid ) = @_;
120              
121             my $p = HTML::TokeParser::Simple->new( string => ($c->response->body || return) );
122              
123             $c->log->debug("Rewriting HTML body with the token parser")
124             if $c->debug;
125              
126             my $tag_map = $c->_session_rewriting_html_tag_map;
127              
128             my $body = '';
129             while ( my $token = $p->get_token ) {
130             if ( my $tag = $token->get_tag ) {
131             # rewrite tags according to the map
132             if ( my $attr_name = $tag_map->{$tag} ) {
133             if ( defined(my $attr_value = $token->get_attr($attr_name) ) ) {
134             $attr_value = $c->uri_with_sessionid($attr_value, $sid)
135             if $c->session_should_rewrite_uri($attr_value);
136              
137             $token->set_attr( $attr_name, $attr_value );
138             }
139             }
140             }
141              
142             $body .= $token->as_is;
143             }
144              
145             $c->response->body($body);
146             }
147              
148             sub rewrite_text_with_session_id {
149             my ( $c, $sid ) = @_;
150              
151             my $body = $c->response->body || return;
152              
153             $c->log->debug("Rewriting plain body with URI::Find")
154             if $c->debug;
155              
156             URI::Find->new(sub {
157             my ( $uri, $orig_uri ) = @_;
158              
159             if ( $c->session_should_rewrite_uri($uri) ) {
160             my $rewritten = $c->uri_with_sessionid($uri, $sid);
161             if ( $orig_uri =~ s/\Q$uri/$rewritten/ ) {
162             # try to keep formatting
163             return $orig_uri;
164             } elsif ( $orig_uri =~ /^(<(?:URI:)?).*(>)$/ ) {
165             return "$1$rewritten$2";
166             } else {
167             return $rewritten;
168             }
169             } else {
170             return $orig_uri;
171             }
172             })->find( \$body );
173              
174             $c->response->body( $body );
175             }
176              
177             sub rewrite_redirect_with_session_id {
178             my ( $c, $sid ) = @_;
179              
180             my $location = $c->response->location || return;
181              
182             $c->log->debug("Rewriting location header")
183             if $c->debug;
184              
185             $c->response->location( $c->uri_with_sessionid($location, $sid) )
186             if $c->session_should_rewrite_uri($location);
187             }
188              
189             sub session_should_rewrite {
190             my $c = shift;
191              
192             my $config = $c->_session_plugin_config;
193             return unless $config->{rewrite_redirect}
194             || $config->{rewrite_body};
195              
196             if ( $c->isa("Catalyst::Plugin::Session::State::Cookie")
197             and $config->{no_rewrite_if_cookie}
198             ) {
199             return if defined($c->get_session_cookie);
200             }
201              
202             return 1;
203             }
204              
205             sub session_should_rewrite_type {
206             my $c = shift;
207              
208             if ( my $types = $c->_session_plugin_config->{rewrite_types} ) {
209             my @req_type = $c->response->content_type; # split
210             foreach my $type ( @$types ) {
211             if ( ref($type) ) {
212             return 1 if $type->( $c, @req_type );
213             } else {
214             return 1 if lc($type) eq $req_type[0];
215             }
216             }
217              
218             return;
219             } else {
220             return 1;
221             }
222             }
223              
224             sub session_should_rewrite_body {
225             my $c = shift;
226             return unless $c->_session_plugin_config->{rewrite_body};
227             return $c->session_should_rewrite_type;
228             }
229              
230             sub session_should_rewrite_redirect {
231             my $c = shift;
232             return unless $c->_session_plugin_config->{rewrite_redirect};
233             ($c->response->status || 0) =~ /^\s*3\d\d\s*$/;
234             }
235              
236              
237             sub uri_for {
238             my ( $c, $path, @args ) = @_;
239              
240             return $c->_session_plugin_config->{overload_uri_for}
241             ? $c->uri_with_sessionid($c->maybe::next::method($path, @args))
242             : $c->maybe::next::method($path, @args);
243             }
244              
245             sub uri_with_sessionid {
246             my ( $c, $uri, $sid ) = @_;
247              
248             $sid ||= $c->sessionid;
249              
250             my $uri_obj = eval { URI->new($uri) } || return $uri;
251              
252             return $c->_session_plugin_config->{param}
253             ? $c->uri_with_param_sessionid($uri_obj, $sid)
254             : $c->uri_with_path_sessionid($uri_obj, $sid);
255             }
256              
257             sub uri_with_param_sessionid {
258             my ( $c, $uri_obj, $sid ) = @_;
259              
260             my $param_name = $c->_session_plugin_config->{param};
261              
262             $uri_obj->query_param( $param_name => $sid );
263              
264             return $uri_obj;
265             }
266              
267             sub uri_with_path_sessionid {
268             my ( $c, $uri_obj, $sid ) = @_;
269              
270             ( my $old_path = $uri_obj->path ) =~ s{/$}{};
271              
272             $uri_obj->path( join( "/-/", $old_path, $sid ) );
273              
274             return $uri_obj;
275             }
276              
277             sub session_should_rewrite_uri {
278             my ( $c, $uri_text ) = @_;
279              
280             my $uri_obj = eval { URI->new($uri_text) } || return;
281              
282             # ignore the url outside
283             my $rel = $uri_obj->abs( $c->request->base );
284              
285             return unless index( $rel, $c->request->base ) == 0;
286              
287             return unless $c->session_should_rewrite_uri_mime_type($rel);
288              
289             if ( my $param = $c->_session_plugin_config->{param} )
290             { # use param style rewriting
291              
292             # if the URI query string doesn't contain $param
293             return not defined $uri_obj->query_param($param);
294              
295             } else { # use path style rewriting
296              
297             # if the URI isn't already rewritten
298             return $uri_obj->path !~ m#/-/#;
299              
300             }
301             }
302              
303             sub session_should_rewrite_uri_mime_type {
304             my ( $c, $uri ) = @_;
305              
306             # ignore media type such as gif, pdf and etc
307             if ( my ($ext) = $uri->path =~ m#\.(\w+)(?:\?|$)# ) {
308             my $mt = MIME::Types->new->mimeTypeOf($ext);
309             return if ref $mt && $mt->isBinary;
310             }
311              
312             return 1;
313             }
314              
315             sub prepare_path {
316             my $c = shift;
317              
318             $c->maybe::next::method(@_);
319              
320             if ( my $param = $c->_session_plugin_config->{param} )
321             { # use param style rewriting
322              
323             if ( my $sid = $c->request->query_parameters->{$param} ) {
324             $c->_sessionid_from_uri($sid);
325             $c->_tried_loading_session_id(0);
326             $c->log->debug(qq/Found sessionid "$sid" in query parameters/)
327             if $c->debug;
328             }
329              
330             } else { # use path style rewriting
331              
332             if ( my ( $path, $sid ) = ( $c->request->path =~ m{^ (?: (.*) / )? -/ (.+) $}x ) ) {
333             $c->request->path( defined($path) ? $path : "" );
334             $c->log->debug(qq/Found sessionid "$sid" in uri path/)
335             if $c->debug;
336             $c->_sessionid_from_uri($sid);
337             $c->_tried_loading_session_id(0);
338             }
339              
340             }
341             }
342              
343             __PACKAGE__
344              
345             __END__
346              
347             =pod
348              
349             =head1 NAME
350              
351             Catalyst::Plugin::Session::State::URI - Use URIs to pass the session id between requests
352              
353             =head1 SYNOPSIS
354              
355             use Catalyst qw/Session Session::State::URI Session::Store::Foo/;
356              
357             # If you want the param style rewriting, set the parameter
358             MyApp->config('Plugin::Session' => {
359             param => 'sessionid', # or whatever you like
360             });
361              
362             =head1 DESCRIPTION
363              
364             In order for L<Catalyst::Plugin::Session> to work the session ID needs
365             to be available on each request, and the session data needs to be
366             stored on the server.
367              
368             This plugin puts the session id into URIs instead of something like a
369             cookie.
370              
371             By default, it rewrites all outgoing URIs, both redirects and in
372             outgoing HTML, but you can exercise control over exactly which URIs
373             are rewritten.
374              
375             =head1 METHODS
376              
377             =over 4
378              
379             =item session_should_rewrite
380              
381             This method is consulted by C<finalize>, and URIs will be rewritten
382             only if it returns a true value.
383              
384             Rewriting is controlled by the C<< $c->config('Plugin::Session' => { rewrite_body => $val })
385             >> and C<< $c->config('Plugin::Session' => { rewrite_redirect => $val }) >> config settings,
386             both of which default to true.
387              
388             To globally disable rewriting simply set these parameters to false.
389              
390             If C<< $c->config('Plugin::Session' => { no_rewrite_if_cookie => 1 }) >>,
391             L<Catalyst::Plugin::Session::State::Cookie> is also in use, and the
392             user agent sent a cookie for the sesion then this method will return
393             false. This parameter also defaults to true.
394              
395             =item session_should_rewrite_body
396              
397             This method checks C<< $c->config('Plugin::Session' => {rewrite_body => $val}) >>
398             first. If this is true, it then calls C<session_should_rewrite_type>.
399              
400             =item session_should_rewrite_type
401              
402             This method determines whether or not the body should be rewritten,
403             based on its content type.
404              
405             For compatibility this method will B<not> test the response's content type
406             without configuration. If you want to do that you must provide a list of valid
407             content types in C<< $c->config->{'Plugin::Session'}{rewrite_types} >>, or subclass this
408             method.
409              
410             =item session_should_rewrite_redirect
411              
412             This method determines whether or not to rewrite the C<Location>
413             header of the response.
414              
415             This method checks C<< $c->config->{session}{rewrite_redirect} >>
416             first. If this is true, it then checks if the status code is a number
417             in the 3xx range.
418              
419             =item session_should_rewrite_uri $uri_text
420              
421             This method is to determine whether a URI should be rewritten.
422              
423             It will return true for URIs under C<$c-E<gt>req-E<gt>base>, and it will also
424             use L<MIME::Types> to filter the links which point to png, pdf and etc with the
425             file extension.
426              
427             You are encouraged to override this method if it's logic doesn't suit your
428             setup.
429              
430             =item session_should_rewrite_uri_mime_type $uri_obj
431              
432             A sub test of session_should_rewrite_uri, that checks if the file name's
433             guessed mime type is of a kind we should rewrite URIs to.
434              
435             Files which are typically static (images, etc) will thus not be rewritten in
436             order to not get 404s or pass bogus parameters to the server.
437              
438             If C<$uri_obj>'s path causes L<MIME::Types> to return true for the C<isBinary>
439             test then then the URI will not be rewritten.
440              
441             =item uri_with_sessionid $uri_text, [ $sid ]
442              
443             When using path style rewriting (the default), it will append
444             C</-/$sessionid> to the uri path.
445              
446             http://myapp/link -> http://myapp/link/-/$sessionid
447              
448             When using param style rewriting, it will add a parameter key/value
449             pair after the uri path.
450              
451             http://myapp/link -> http://myapp/link?$param=$sessionid
452              
453             If $sid is not provided it will default to C<< $c->sessionid >>.
454              
455             =item session_rewrite_if_needed
456              
457             Rewrite the response if necessary.
458              
459             =item rewrite_body_with_session_id $sid
460              
461             Calls either C<rewrite_html_with_session_id> or C<rewrite_text_with_session_id>
462             depending on the content type.
463              
464             =item rewrite_html_with_session_id $sid
465              
466             Rewrites the body using L<HTML::TokePaser::Simple>.
467              
468             This method of rewriting also matches relative URIs, and is thus more robust.
469              
470             =item rewrite_text_with_session_id $sid
471              
472             Rewrites the body using L<URI::Find>.
473              
474             This method is used when the content does not appear to be HTML.
475              
476             =item rewrite_redirect_with_session_id $sid
477              
478             Rewrites the C<Location> header.
479              
480             =item uri_with_param_sessionid
481              
482             =item uri_with_path_sessionid
483              
484             =back
485              
486             =head1 EXTENDED METHODS
487              
488             =over 4
489              
490             =item prepare_path
491              
492             Will restore the session if the request URI is formatted accordingly, and
493             rewrite the URI to remove the additional part.
494              
495             =item finalize
496              
497             Rewrite a redirect or the body HTML as appropriate.
498              
499             =item delete_session_id
500              
501             =item get_session_id
502              
503             =item set_session_id
504              
505             =item setup_session
506              
507             =item uri_for
508              
509             =back
510              
511             =head1 CAVEATS
512              
513             =head2 Session Hijacking
514              
515             URI sessions are very prone to session hijacking problems.
516              
517             Make sure your users know not to copy and paste URIs to prevent these problems,
518             and always provide a way to safely link to public resources.
519              
520             Also make sure to never link to external sites without going through a gateway
521             page that does not have session data in it's URI, so that the external site
522             doesn't get any session IDs in the http referrer header.
523              
524             Due to these issues this plugin should be used as a last resort, as
525             L<Catalyst::Plugin::Session::State::Cookie> is more appropriate 99% of the
526             time.
527              
528             Take a look at the IP address limiting features in L<Catalyst::Plugin::Session>
529             to see make some of these problems less dangerous.
530              
531             =head3 Goodbye page recipe
532              
533             To exclude some sections of your application, like a goodbye page (see
534             L</CAVEATS>) you should make extend the C<session_should_rewrite_uri> method to
535             return true if the URI does not point to the goodbye page, extend
536             C<prepare_path> to not rewrite URIs that match C</-/> (so that external URIs
537             with that in their path as a parameter to the goodbye page will not be
538             destroyed) and finally extend C<uri_with_sessionid> to rewrite URIs with the
539             following logic:
540              
541             =over 4
542              
543             =item *
544              
545             URIs that match C</^$base/> are appended with session data (
546             C<< $c->maybe::next::method >>).
547              
548             =item *
549              
550             External URIs (everything else) should be prepended by the goodbye page. (e.g.
551             C<http://myapp/link/http://the_url_of_whatever/foo.html>).
552              
553             =back
554              
555             But note that this behavior will be problematic when you are e.g. submitting
556             POSTs to forms on external sites.
557              
558             =head1 SEE ALSO
559              
560             L<Catalyst>, L<Catalyst::Plugin::Session>,L<Catalyst::Plugin::Session::FastMmap>
561             C<HTML::TokeParser::Simple>, C<MIME::Types>.
562              
563             =head1 AUTHORS
564              
565             This module is derived from L<Catalyst::Plugin::Session::FastMmap> code, and
566             has been heavily modified since.
567              
568             =over 4
569              
570             =item Andrew Ford
571              
572             =item Andy Grundman
573              
574             =item Christian Hansen
575              
576             =item Dave Rolsky
577              
578             =item Yuval Kogman, C<nothingmuch@woobling.org>
579              
580             =item Marcus Ramberg
581              
582             =item Sebastian Riedel
583              
584             =item Hu Hailin
585              
586             =item Tomas Doran, C<bobtfish@bobtfish.net> (Current maintainer)
587              
588             =item Florian Ragwitz C<rafl@debian.org>
589              
590             =back
591              
592             =head1 COPYRIGHT
593              
594             This program is free software, you can redistribute it and/or modify it
595             under the same terms as Perl itself.
596              
597             =cut
598              
599             1;