File Coverage

blib/lib/Plack/Middleware/RedirectSSL.pm
Criterion Covered Total %
statement 72 73 98.6
branch 50 50 100.0
condition 29 30 96.6
subroutine 15 15 100.0
pod 5 5 100.0
total 171 173 98.8


line stmt bran cond sub pod time code
1 3     3   243576 use 5.006; use strict; use warnings;
  3     3   24  
  3     3   12  
  3         5  
  3         49  
  3         14  
  3         5  
  3         202  
2              
3             package Plack::Middleware::RedirectSSL;
4              
5             our $VERSION = '1.302';
6              
7 3     3   384 BEGIN { require Plack::Middleware; our @ISA = 'Plack::Middleware' }
  3         12717  
8              
9 3     3   17 use Plack::Util ();
  3         4  
  3         74  
10 3     3   14 use Plack::Util::Accessor qw( ssl hsts_header );
  3         6  
  3         16  
11 3     3   1328 use Plack::Request ();
  3         181704  
  3         1954  
12              
13             # seconds minutes hours days weeks
14             sub DEFAULT_STS_MAXAGE () { 60 * 60 * 24 * 7 * 26 }
15             sub MIN_STS_PRELOAD_MAXAGE () { 60 * 60 * 24 * 365 }
16              
17             sub call {
18 22     22 1 66620 my ( $self, $env ) = ( shift, @_ );
19              
20 22 100       55 my $do_ssl = $self->ssl ? 1 : 0;
21 22 100       141 my $is_ssl = ( 'https' eq $env->{'psgi.url_scheme'} ) ? 1 : 0;
22              
23 22 100 100     104 if ( $is_ssl xor $do_ssl ) {
24 9         18 my $m = $env->{'REQUEST_METHOD'};
25 9 100 100     42 return [ 400, [qw( Content-Type text/plain )], [ 'Bad Request' ] ]
26             if 'GET' ne $m and 'HEAD' ne $m;
27 7         41 my $uri = Plack::Request->new( $env )->uri;
28 7 100       1628 $uri->scheme( $do_ssl ? 'https' : 'http' );
29 7         2041 return [ 301, [ Location => $uri ], [] ];
30             }
31              
32 13         42 my $res = $self->app->( $env );
33              
34 13 100 100     106 return $res unless $is_ssl and my $hsts = $self->hsts_header;
35              
36             Plack::Util::response_cb( $res, sub {
37 9     9   141 Plack::Util::header_set( $_[0][1], 'Strict-Transport-Security', $hsts );
38 9         82 } );
39             }
40              
41             sub hsts_policy {
42 20     20 1 40 my ( $self, $policy ) = ( shift, @_ );
43 20 100       53 return $self->{'hsts_policy'} unless @_;
44 16         35 $self->hsts_header( render_sts_policy( $policy ) );
45 15 100 100     100 $self->{'hsts'} = $policy ? $policy->{'max_age'} || '00' : 0; # legacy compat
46 15         30 $self->{'hsts_policy'} = $policy;
47             }
48              
49             sub hsts {
50 10     10 1 1899 my ( $self, $value ) = ( shift, @_ );
51 10 100       27 return $self->{'hsts'} unless @_;
52             $self->hsts_policy( ( $value or not defined $value )
53 8 100 100     56 ? { ( map %$_, $self->{'hsts_policy'} || () ), max_age => $value }
      66        
54             : undef
55             );
56 8         15 $self->{'hsts'} = $value;
57             }
58              
59             sub new {
60 11     11 1 4050 my $self = shift->SUPER::new( @_ );
61 11 100       133 $self->ssl(1) if not defined $self->ssl;
62 11 100       217 if ( exists $self->{'hsts_policy'} ) { $self->hsts_policy( $self->{'hsts_policy'} ) }
  5 100       10  
    100          
63 2         5 elsif ( exists $self->{'hsts'} ) { $self->hsts ( $self->{'hsts'} ) }
64 3         22 elsif ( not $self->hsts_header ) { $self->hsts_policy( {} ) }
65 10         30 $self;
66             }
67              
68             ########################################################################
69              
70 10 100   10   13 sub _callsite () { my $i; while ( my ( $p, $f, $l ) = caller ++$i ) { return " at $f line $l.\n" if __PACKAGE__ ne $p } '' }
  10         63  
  12         114  
  0         0  
71              
72             sub render_sts_policy {
73 40     40 1 924 my ( $opt ) = @_;
74              
75 40 100 100     212 die 'HSTS policy must be a single undef value or hash ref', _callsite
      100        
76             if 1 != @_ or defined $opt and 'HASH' ne ref $opt;
77              
78 35 100       83 return undef if not defined $opt;
79              
80 28         62 my @directive = qw( max_age include_subdomains preload );
81              
82             {
83 28         44 my %known = map +( $_, 1 ), @directive;
  28         131  
84 28         116 my $unknown = join ', ', map "'$_'", sort grep !$known{ $_ }, keys %$opt;
85 28 100       79 die "HSTS policy contains unknown directive(s) $unknown", _callsite if $unknown;
86             }
87              
88 27         64 my ( $max_age, $include_subdomains, $preload ) = @$opt{ @directive };
89              
90             $max_age = defined $max_age
91 3 100   3   27 ? do { no warnings 'numeric'; int $max_age }
  3 100       18  
  3         532  
  27         61  
  13         26  
92             : $preload ? MIN_STS_PRELOAD_MAXAGE : DEFAULT_STS_MAXAGE;
93              
94 27 100 100     67 die 'HSTS max_age 0 conflicts with setting other directives', _callsite
      100        
95             if 0 == $max_age and ( $include_subdomains or $preload );
96              
97 25 100       45 if ( $preload ) {
98 7 100       14 $include_subdomains = 1 unless defined $include_subdomains;
99 7 100       15 die 'HSTS preload conflicts with disabled include_subdomains', _callsite unless $include_subdomains;
100 6 100       11 die "HSTS preload requires longer max_age (got $max_age; minimum ".MIN_STS_PRELOAD_MAXAGE.')', _callsite
101             if MIN_STS_PRELOAD_MAXAGE > $max_age;
102             }
103              
104             # expose computed values back to the caller
105 23         74 @$opt{ @directive } = ( $max_age, !!$include_subdomains, !!$preload );
106              
107 23         123 join '; ', "max-age=$max_age", ('includeSubDomains') x !!$include_subdomains, ('preload') x !!$preload;
108             }
109              
110             1;
111              
112             __END__