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