Plack
view release on metacpan or search on metacpan
lib/Plack/Middleware/Lint.pm view on Meta::CPAN
sub call {
my $self = shift;
my $env = shift;
$self->validate_env($env);
my $res = $self->app->($env);
return $self->validate_res($res);
}
sub validate_env {
my ($self, $env) = @_;
unless ($env->{REQUEST_METHOD}) {
die('Missing env param: REQUEST_METHOD');
}
unless ($env->{REQUEST_METHOD} =~ /^[A-Z]+$/) {
die("Invalid env param: REQUEST_METHOD($env->{REQUEST_METHOD})");
}
unless (defined($env->{SCRIPT_NAME})) { # allows empty string
die('Missing mandatory env param: SCRIPT_NAME');
}
if ($env->{SCRIPT_NAME} eq '/') {
die('SCRIPT_NAME must not be /');
}
unless (defined($env->{PATH_INFO})) { # allows empty string
die('Missing mandatory env param: PATH_INFO');
}
if ($env->{PATH_INFO} ne '' && $env->{PATH_INFO} !~ m!^/!) {
die('PATH_INFO must begin with / ($env->{PATH_INFO})');
}
unless (defined($env->{SERVER_NAME})) {
die('Missing mandatory env param: SERVER_NAME');
}
if ($env->{SERVER_NAME} eq '') {
die('SERVER_NAME must not be empty string');
}
unless (defined($env->{SERVER_PORT})) {
die('Missing mandatory env param: SERVER_PORT');
}
if ($env->{SERVER_PORT} eq '') {
die('SERVER_PORT must not be empty string');
}
if (defined($env->{SERVER_PROTOCOL}) and $env->{SERVER_PROTOCOL} !~ m{^HTTP/\d}) {
die("Invalid SERVER_PROTOCOL: $env->{SERVER_PROTOCOL}");
}
for my $param (qw/version url_scheme input errors multithread multiprocess/) {
unless (exists $env->{"psgi.$param"}) {
die("Missing psgi.$param");
}
}
unless (ref($env->{'psgi.version'}) eq 'ARRAY') {
die("psgi.version should be ArrayRef: $env->{'psgi.version'}");
}
unless (scalar(@{$env->{'psgi.version'}}) == 2) {
die('psgi.version should contain 2 elements, not ', scalar(@{$env->{'psgi.version'}}));
}
unless ($env->{'psgi.url_scheme'} =~ /^https?$/) {
die("psgi.url_scheme should be 'http' or 'https': ", $env->{'psgi.url_scheme'});
}
if ($env->{"psgi.version"}->[1] == 1) { # 1.1
for my $param (qw(streaming nonblocking run_once)) {
unless (exists $env->{"psgi.$param"}) {
die("Missing psgi.$param");
}
}
}
if ($env->{HTTP_CONTENT_TYPE}) {
die('HTTP_CONTENT_TYPE should not exist');
}
if ($env->{HTTP_CONTENT_LENGTH}) {
die('HTTP_CONTENT_LENGTH should not exist');
}
}
sub is_possibly_fh {
my $fh = shift;
ref $fh eq 'GLOB' &&
*{$fh}{IO} &&
*{$fh}{IO}->can('getline');
}
sub validate_res {
my ($self, $res, $streaming) = @_;
unless (ref($res) eq 'ARRAY' or ref($res) eq 'CODE') {
die("Response should be array ref or code ref: $res");
}
if (ref $res eq 'CODE') {
return $self->response_cb($res, sub { $self->validate_res(@_, 1) });
}
unless (@$res == 3 || ($streaming && @$res == 2)) {
die('Response needs to be 3 element array, or 2 element in streaming');
}
unless ($res->[0] =~ /^\d+$/ && $res->[0] >= 100) {
die("Status code needs to be an integer greater than or equal to 100: $res->[0]");
}
unless (ref $res->[1] eq 'ARRAY') {
die("Headers needs to be an array ref: $res->[1]");
}
my @copy = @{$res->[1]};
unless (@copy % 2 == 0) {
die('The number of response headers needs to be even, not odd(', scalar(@copy), ')');
}
while(my($key, $val) = splice(@copy, 0, 2)) {
if (lc $key eq 'status') {
die('Response headers MUST NOT contain a key named Status');
}
if ($key =~ /[:\r\n]|[-_]$/) {
die("Response headers MUST NOT contain a key with : or newlines, or that end in - or _. Header: $key");
}
unless ($key =~ /^[a-zA-Z][0-9a-zA-Z\-_]*$/) {
die("Response headers MUST consist only of letters, digits, _ or - and MUST start with a letter. Header: $key");
}
if ($val =~ /[\000-\037]/) {
die("Response headers MUST NOT contain characters below octal \037. Header: $key. Value: $val");
}
unless (defined $val) {
die("Response headers MUST be a defined string. Header: $key");
}
}
# @$res == 2 is only right in psgi.streaming, and it's already checked.
unless (@$res == 2 ||
ref $res->[2] eq 'ARRAY' ||
Plack::Util::is_real_fh($res->[2]) ||
is_possibly_fh($res->[2]) ||
(blessed($res->[2]) && $res->[2]->can('getline'))) {
die("Body should be an array ref or filehandle: $res->[2]");
}
if (ref $res->[2] eq 'ARRAY' && grep _has_wide_char($_), @{$res->[2]}) {
die("Body must be bytes and should not contain wide characters (UTF-8 strings)");
}
return $res;
}
# NOTE: Some modules like HTML:: or XML:: could possibly generate
# ASCII/Latin-1 strings with utf8 flags on. They're actually safe to
# print, so there's no need to give warnings about it.
sub _has_wide_char {
my $str = shift;
utf8::is_utf8($str) && $str =~ /[^\x00-\xff]/;
}
1;
__END__
=head1 NAME
Plack::Middleware::Lint - Validate request and response
=head1 SYNOPSIS
use Plack::Middleware::Lint;
my $app = sub { ... }; # your app or middleware
$app = Plack::Middleware::Lint->wrap($app);
# Or from plackup
plackup -e 'enable "Lint"' myapp.psgi
=head1 DESCRIPTION
Plack::Middleware::Lint is a middleware component to validate request
and response environment formats. You are strongly suggested to use
this middleware when you develop a new framework adapter or a new PSGI
web server that implements the PSGI interface.
This middleware is enabled by default when you run plackup or other
launcher tools with the default environment I<development> value.
=head1 DEBUGGING
Because of how this middleware works, it may not be easy to debug Lint
errors when you encounter one, unless you're writing a PSGI web server
or a framework.
For example, when you're an application developer (user of some
framework) and see errors like:
Body should be an array ref or filehandle at lib/Plack/Middleware/Lint.pm line XXXX
( run in 0.569 second using v1.01-cache-2.11-cpan-39bf76dae61 )