File Coverage

blib/lib/Amon2/Plugin/Web/JSON.pm
Criterion Covered Total %
statement 35 35 100.0
branch 11 12 91.6
condition 12 13 92.3
subroutine 6 6 100.0
pod 0 1 0.0
total 64 67 95.5


line stmt bran cond sub pod time code
1             use strict;
2 5     5   2418 use warnings;
  5         11  
  5         146  
3 5     5   23 use JSON 2 qw/encode_json/;
  5         11  
  5         159  
4 5     5   81 use Amon2::Util ();
  5         126  
  5         45  
5 5     5   737  
  5         10  
  5         2396  
6             my $_JSON = JSON->new()->ascii(1);
7              
8             my %_ESCAPE = (
9             '+' => '\\u002b', # do not eval as UTF-7
10             '<' => '\\u003c', # do not eval as HTML
11             '>' => '\\u003e', # ditto.
12             );
13              
14             my ($class, $c, $conf) = @_;
15             unless ($c->can('render_json')) {
16 9     9 0 25 Amon2::Util::add_method($c, 'render_json', sub {
17 9 50       108 my ($c, $stuff) = @_;
18              
19 19     19   20628 # for IE7 JSON venularity.
20             # see http://www.atmarkit.co.jp/fcoding/articles/webapp/05/webapp05a.html
21             my $output = $_JSON->canonical( $conf->{canonical} ? 1 : 0 )->encode($stuff);
22             $output =~ s!([+<>])!$_ESCAPE{$1}!g;
23 19 100       225  
24 19         79 my $user_agent = $c->req->user_agent || '';
25              
26 19   100     87 # defense from JSON hijacking
27             if ((!$c->request->header('X-Requested-With')) && $user_agent =~ /android/i && defined $c->req->header('Cookie') && ($c->req->method||'GET') eq 'GET') {
28             my $res = $c->create_response(403);
29 19 100 100     1278 $res->content_type('text/html; charset=utf-8');
      66        
      100        
      100        
30 1         21 $res->content("Your request may be JSON hijacking.\nIf you are not an attacker, please add 'X-Requested-With' header to each request.");
31 1         29 $res->content_length(length $res->content);
32 1         52 return $res;
33 1         14 }
34 1         66  
35             my $res = $c->create_response(200);
36              
37 18         814 my $encoding = $c->encoding();
38             $encoding = lc($encoding->mime_name) if ref $encoding;
39 18         346 $res->content_type("application/json; charset=$encoding");
40 18 100       111 $res->header( 'X-Content-Type-Options' => 'nosniff' ); # defense from XSS
41 18         256 $res->content_length(length($output));
42 18         495 $res->body($output);
43 18         827  
44 18         598 if (defined (my $status_code_field = $conf->{status_code_field})) {
45             $res->header( 'X-API-Status' => $stuff->{$status_code_field} ) if exists $stuff->{$status_code_field};
46 18 100       117 }
47 6 100       21  
48             return $res;
49             });
50 18         148 }
51 9         83 }
52              
53             1;
54              
55             =encoding utf-8
56              
57             =head1 NAME
58              
59             Amon2::Plugin::Web::JSON - JSON plugin
60              
61             =head1 SYNOPSIS
62              
63             use Amon2::Lite;
64              
65             __PACKAGE__->load_plugins(qw/Web::JSON/);
66              
67             get '/' => sub {
68             my $c = shift;
69             return $c->render_json(+{foo => 'bar'});
70             };
71              
72             __PACKAGE__->to_app();
73              
74             =head1 DESCRIPTION
75              
76             This is a JSON plugin.
77              
78             =head1 METHODS
79              
80             =over 4
81              
82             =item C<< $c->render_json(\%dat); >>
83              
84             Generate JSON data from C<< \%dat >> and returns instance of L<Plack::Response>.
85              
86             =back
87              
88             =head1 PARAMETERS
89              
90             =over 4
91              
92             =item status_code_field
93              
94             It specify the field name of JSON to be embedded in the 'X-API-Status' header.
95             Default is C<< undef >>. If you set the C<< undef >> to disable this 'X-API-Status' header.
96              
97             __PACKAGE__->load_plugins(
98             'Web::JSON' => { status_code_field => 'status' }
99             );
100             ...
101             $c->render_json({ status => 200, message => 'ok' })
102             # send response header 'X-API-Status: 200'
103              
104             In general JSON API error code embed in a JSON by JSON API Response body.
105             But can not be logging the error code of JSON for the access log of a general Web Servers.
106             You can possible by using the 'X-API-Status' header.
107              
108             =item canonical
109              
110             If canonical parameter is true, then this plugin will output JSON objects by sorting their keys.
111             This is adding a comparatively high overhead.
112              
113             __PACKAGE__->load_plugins(
114             'Web::JSON' => { canonical => 1 }
115             );
116             ...
117             $c->render_json({ b => 1, c => 1, a => 1 });
118             # json response is '{ "a" : 1, "b" : 1, "c" : 1 }'
119              
120             =back
121              
122             =head1 FAQ
123              
124             =over 4
125              
126             =item How can I use JSONP?
127              
128             You can use JSONP by using L<Plack::Middleware::JSONP>.
129              
130             =back
131              
132             =head1 JSON and security
133              
134             =over 4
135              
136             =item Browse the JSON files directly.
137              
138             This module escapes '<', '>', and '+' characters by "\uXXXX" form. Browser don't detects the JSON as HTML.
139              
140             And also this module outputs C<< X-Content-Type-Options: nosniff >> header for IEs.
141              
142             It's good enough, I hope.
143              
144             =item JSON Hijacking
145              
146             Latest browsers doesn't have a JSON hijacking issue(I hope). __defineSetter__ or UTF-7 attack was resolved by browsers.
147              
148             But Firefox<=3.0.x and Android phones have issue on Array constructor, see L<http://d.hatena.ne.jp/ockeghem/20110907/p1>.
149              
150             Firefox<=3.0.x was outdated. Web application developers doesn't need to add work-around for it, see L<http://en.wikipedia.org/wiki/Firefox#Version_release_table>.
151              
152             L<Amon2::Plugin::Web::JSON> have a JSON hijacking detection feature. Amon2::Plugin::Web::JSON returns "403 Forbidden" response if following pattern request.
153              
154             =over 4
155              
156             =item The request have 'Cookie' header.
157              
158             =item The request doesn't have 'X-Requested-With' header.
159              
160             =item The request contains /android/i string in 'User-Agent' header.
161              
162             =item Request method is 'GET'
163              
164             =back
165              
166             =back
167              
168             See also the L<hasegawayosuke's article(Japanese)|http://www.atmarkit.co.jp/fcoding/articles/webapp/05/webapp05a.html>.
169              
170             =head1 FAQ
171              
172             =over 4
173              
174             =item HOW DO YOU CHANGE THE HTTP STATUS CODE FOR JSON?
175              
176             render_json method returns instance of Plack::Response. You can modify the response object.
177              
178             Here is a example code:
179              
180             get '/' => sub {
181             my $c = shift;
182             if (-f '/tmp/maintenance') {
183             my $res = $c->render_json({err => 'Under maintenance'});
184             $res->status(503);
185             return $res;
186             }
187             return $c->render_json({err => undef});
188             };
189              
190             =back
191              
192             =head1 THANKS TO
193              
194             hasegawayosuke
195