File Coverage

blib/lib/Plack/Middleware/Lint.pm
Criterion Covered Total %
statement 96 108 88.8
branch 56 72 77.7
condition 29 41 70.7
subroutine 13 13 100.0
pod 1 5 20.0
total 195 239 81.5


line stmt bran cond sub pod time code
1             package Plack::Middleware::Lint;
2 44     44   165429 use strict;
  44         138  
  44         1282  
3 44     44   220 no warnings;
  44         88  
  44         1243  
4 44     44   229 use Carp ();
  44         88  
  44         867  
5 44     44   699 use parent qw(Plack::Middleware);
  44         373  
  44         280  
6 44     44   2012 use Scalar::Util qw(blessed reftype);
  44         94  
  44         2045  
7 44     44   260 use Plack::Util;
  44         48  
  44         56168  
8              
9             sub wrap {
10 27     27 0 5503 my($self, $app) = @_;
11              
12 27 50 33     107 unless (reftype $app eq 'CODE' or overload::Method($app, '&{}')) {
13 0 0       0 die("PSGI app should be a code reference: ", (defined $app ? $app : "undef"));
14             }
15              
16 27         96 $self->SUPER::wrap($app);
17             }
18              
19             sub call {
20 34     34 1 57 my $self = shift;
21 34         51 my $env = shift;
22              
23 34         100 $self->validate_env($env);
24 26         114 my $res = $self->app->($env);
25 26         277 return $self->validate_res($res);
26             }
27              
28             sub validate_env {
29 34     34 0 62 my ($self, $env) = @_;
30 34 100       94 unless ($env->{REQUEST_METHOD}) {
31 1         9 die('Missing env param: REQUEST_METHOD');
32             }
33 33 100       152 unless ($env->{REQUEST_METHOD} =~ /^[A-Z]+$/) {
34 1         9 die("Invalid env param: REQUEST_METHOD($env->{REQUEST_METHOD})");
35             }
36 32 50       91 unless (defined($env->{SCRIPT_NAME})) { # allows empty string
37 0         0 die('Missing mandatory env param: SCRIPT_NAME');
38             }
39 32 100       76 if ($env->{SCRIPT_NAME} eq '/') {
40 1         8 die('SCRIPT_NAME must not be /');
41             }
42 31 50       87 unless (defined($env->{PATH_INFO})) { # allows empty string
43 0         0 die('Missing mandatory env param: PATH_INFO');
44             }
45 31 100 100     177 if ($env->{PATH_INFO} ne '' && $env->{PATH_INFO} !~ m!^/!) {
46 1         9 die('PATH_INFO must begin with / ($env->{PATH_INFO})');
47             }
48 30 50       67 unless (defined($env->{SERVER_NAME})) {
49 0         0 die('Missing mandatory env param: SERVER_NAME');
50             }
51 30 50       73 if ($env->{SERVER_NAME} eq '') {
52 0         0 die('SERVER_NAME must not be empty string');
53             }
54 30 100       65 unless (defined($env->{SERVER_PORT})) {
55 1         9 die('Missing mandatory env param: SERVER_PORT');
56             }
57 29 50       87 if ($env->{SERVER_PORT} eq '') {
58 0         0 die('SERVER_PORT must not be empty string');
59             }
60 29 100 66     171 if (defined($env->{SERVER_PROTOCOL}) and $env->{SERVER_PROTOCOL} !~ m{^HTTP/\d}) {
61 1         9 die("Invalid SERVER_PROTOCOL: $env->{SERVER_PROTOCOL}");
62             }
63 28         66 for my $param (qw/version url_scheme input errors multithread multiprocess/) {
64 168 50       374 unless (exists $env->{"psgi.$param"}) {
65 0         0 die("Missing psgi.$param");
66             }
67             }
68 28 100       89 unless (ref($env->{'psgi.version'}) eq 'ARRAY') {
69 1         10 die("psgi.version should be ArrayRef: $env->{'psgi.version'}");
70             }
71 27 50       39 unless (scalar(@{$env->{'psgi.version'}}) == 2) {
  27         73  
72 0         0 die('psgi.version should contain 2 elements, not ', scalar(@{$env->{'psgi.version'}}));
  0         0  
73             }
74 27 50       103 unless ($env->{'psgi.url_scheme'} =~ /^https?$/) {
75 0         0 die("psgi.url_scheme should be 'http' or 'https': ", $env->{'psgi.url_scheme'});
76             }
77 27 50       83 if ($env->{"psgi.version"}->[1] == 1) { # 1.1
78 27         48 for my $param (qw(streaming nonblocking run_once)) {
79 81 50       188 unless (exists $env->{"psgi.$param"}) {
80 0         0 die("Missing psgi.$param");
81             }
82             }
83             }
84 27 100       56 if ($env->{HTTP_CONTENT_TYPE}) {
85 1         8 die('HTTP_CONTENT_TYPE should not exist');
86             }
87 26 50       75 if ($env->{HTTP_CONTENT_LENGTH}) {
88 0         0 die('HTTP_CONTENT_LENGTH should not exist');
89             }
90             }
91              
92             sub is_possibly_fh {
93 5     5 0 48 my $fh = shift;
94              
95             ref $fh eq 'GLOB' &&
96 1         7 *{$fh}{IO} &&
97 5 100 100     38 *{$fh}{IO}->can('getline');
  1         13  
98             }
99              
100             sub validate_res {
101 28     28 0 63 my ($self, $res, $streaming) = @_;
102              
103 28 100 100     105 unless (ref($res) eq 'ARRAY' or ref($res) eq 'CODE') {
104 1         12 die("Response should be array ref or code ref: $res");
105             }
106              
107 27 100       64 if (ref $res eq 'CODE') {
108 2     2   16 return $self->response_cb($res, sub { $self->validate_res(@_, 1) });
  2         5  
109             }
110              
111 25 50 33     64 unless (@$res == 3 || ($streaming && @$res == 2)) {
      66        
112 1         9 die('Response needs to be 3 element array, or 2 element in streaming');
113             }
114              
115 24 100 66     168 unless ($res->[0] =~ /^\d+$/ && $res->[0] >= 100) {
116 1         12 die("Status code needs to be an integer greater than or equal to 100: $res->[0]");
117             }
118              
119 23 100       86 unless (ref $res->[1] eq 'ARRAY') {
120 1         12 die("Headers needs to be an array ref: $res->[1]");
121             }
122              
123 22         43 my @copy = @{$res->[1]};
  22         53  
124 22 100       86 unless (@copy % 2 == 0) {
125 1         11 die('The number of response headers needs to be even, not odd(', scalar(@copy), ')');
126             }
127              
128 21         88 while(my($key, $val) = splice(@copy, 0, 2)) {
129 16 100       55 if (lc $key eq 'status') {
130 1         11 die('Response headers MUST NOT contain a key named Status');
131             }
132 15 100       69 if ($key =~ /[:\r\n]|[-_]$/) {
133 4         44 die("Response headers MUST NOT contain a key with : or newlines, or that end in - or _. Header: $key");
134             }
135 11 100       70 unless ($key =~ /^[a-zA-Z][0-9a-zA-Z\-_]*$/) {
136 2         22 die("Response headers MUST consist only of letters, digits, _ or - and MUST start with a letter. Header: $key");
137             }
138 9 100       46 if ($val =~ /[\000-\037]/) {
139 2         25 die("Response headers MUST NOT contain characters below octal \037. Header: $key. Value: $val");
140             }
141 7 100       37 unless (defined $val) {
142 2         23 die("Response headers MUST be a defined string. Header: $key");
143             }
144             }
145              
146             # @$res == 2 is only right in psgi.streaming, and it's already checked.
147 10 50 66     76 unless (@$res == 2 ||
      66        
      100        
      33        
      66        
148             ref $res->[2] eq 'ARRAY' ||
149             Plack::Util::is_real_fh($res->[2]) ||
150             is_possibly_fh($res->[2]) ||
151             (blessed($res->[2]) && $res->[2]->can('getline'))) {
152 4         75 die("Body should be an array ref or filehandle: $res->[2]");
153             }
154              
155 6 100 100     30 if (ref $res->[2] eq 'ARRAY' && grep _has_wide_char($_), @{$res->[2]}) {
  5         24  
156 1         10 die("Body must be bytes and should not contain wide characters (UTF-8 strings)");
157             }
158              
159 5         36 return $res;
160             }
161              
162             # NOTE: Some modules like HTML:: or XML:: could possibly generate
163             # ASCII/Latin-1 strings with utf8 flags on. They're actually safe to
164             # print, so there's no need to give warnings about it.
165             sub _has_wide_char {
166 5     5   20 my $str = shift;
167 5 100       83 utf8::is_utf8($str) && $str =~ /[^\x00-\xff]/;
168             }
169              
170             1;
171             __END__