File Coverage

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


line stmt bran cond sub pod time code
1 3     3   381131 use 5.006; use strict; use warnings;
  3     3   38  
  3     3   18  
  3         6  
  3         81  
  3         16  
  3         6  
  3         198  
2              
3             package Plack::Middleware::RedirectSSL;
4             $Plack::Middleware::RedirectSSL::VERSION = '1.301';
5             # ABSTRACT: force all requests to use in-/secure connections
6              
7 3     3   597 use parent 'Plack::Middleware';
  3         477  
  3         21  
8              
9 3     3   17373 use Plack::Util ();
  3         18  
  3         84  
10 3     3   20 use Plack::Util::Accessor qw( ssl hsts_header );
  3         7  
  3         24  
11 3     3   1892 use Plack::Request ();
  3         250896  
  3         2379  
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 80259 my ( $self, $env ) = ( shift, @_ );
19              
20 22 100       71 my $do_ssl = $self->ssl ? 1 : 0;
21 22 100       171 my $is_ssl = ( 'https' eq $env->{'psgi.url_scheme'} ) ? 1 : 0;
22              
23 22 100 100     118 if ( $is_ssl xor $do_ssl ) {
24 9         19 my $m = $env->{'REQUEST_METHOD'};
25 9 100 100     47 return [ 400, [qw( Content-Type text/plain )], [ 'Bad Request' ] ]
26             if 'GET' ne $m and 'HEAD' ne $m;
27 7         43 my $uri = Plack::Request->new( $env )->uri;
28 7 100       1985 $uri->scheme( $do_ssl ? 'https' : 'http' );
29 7         1676 return [ 301, [ Location => $uri ], [] ];
30             }
31              
32 13         51 my $res = $self->app->( $env );
33              
34 13 100 100     186 return $res unless $is_ssl and my $hsts = $self->hsts_header;
35              
36             Plack::Util::response_cb( $res, sub {
37 9     9   163 Plack::Util::header_set( $_[0][1], 'Strict-Transport-Security', $hsts );
38 9         101 } );
39             }
40              
41             sub hsts_policy {
42 20     20 1 59 my ( $self, $policy ) = ( shift, @_ );
43 20 100       67 return $self->{'hsts_policy'} unless @_;
44 16         45 $self->hsts_header( render_sts_policy( $policy ) );
45 15 100 100     137 $self->{'hsts'} = $policy ? $policy->{'max_age'} || '00' : 0; # legacy compat
46 15         39 $self->{'hsts_policy'} = $policy;
47             }
48              
49             sub hsts {
50 10     10 1 2707 my ( $self, $value ) = ( shift, @_ );
51 10 100       37 return $self->{'hsts'} unless @_;
52             $self->hsts_policy( ( $value or not defined $value )
53 8 100 100     75 ? { ( map %$_, $self->{'hsts_policy'} || () ), max_age => $value }
      66        
54             : undef
55             );
56 8         23 $self->{'hsts'} = $value;
57             }
58              
59             sub new {
60 11     11 1 5735 my $self = shift->SUPER::new( @_ );
61 11 100       178 $self->ssl(1) if not defined $self->ssl;
62 11 100       276 if ( exists $self->{'hsts_policy'} ) { $self->hsts_policy( $self->{'hsts_policy'} ) }
  5 100       17  
    100          
63 2         8 elsif ( exists $self->{'hsts'} ) { $self->hsts ( $self->{'hsts'} ) }
64 3         26 elsif ( not $self->hsts_header ) { $self->hsts_policy( {} ) }
65 10         38 $self;
66             }
67              
68             ########################################################################
69              
70 10 100   10   17 sub _callsite () { my $i; while ( my ( $p, $f, $l ) = caller ++$i ) { return " at $f line $l.\n" if __PACKAGE__ ne $p } '' }
  10         81  
  12         149  
  0         0  
71              
72             sub render_sts_policy {
73 40     40 1 1239 my ( $opt ) = @_;
74              
75 40 100 100     249 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       105 return undef if not defined $opt;
79              
80 28         119 my @directive = qw( max_age include_subdomains preload );
81              
82             {
83 28         54 my %known = map +( $_, 1 ), @directive;
  28         140  
84 28         147 my $unknown = join ', ', map "'$_'", sort grep !$known{ $_ }, keys %$opt;
85 28 100       103 die "HSTS policy contains unknown directive(s) $unknown", _callsite if $unknown;
86             }
87              
88 27         79 my ( $max_age, $include_subdomains, $preload ) = @$opt{ @directive };
89              
90             $max_age = defined $max_age
91 3 100   3   34 ? do { no warnings 'numeric'; int $max_age }
  3 100       9  
  3         651  
  27         82  
  13         28  
92             : $preload ? MIN_STS_PRELOAD_MAXAGE : DEFAULT_STS_MAXAGE;
93              
94 27 100 100     108 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       58 if ( $preload ) {
98 7 100       16 $include_subdomains = 1 unless defined $include_subdomains;
99 7 100       17 die 'HSTS preload conflicts with disabled include_subdomains', _callsite unless $include_subdomains;
100 6 100       17 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         98 @$opt{ @directive } = ( $max_age, !!$include_subdomains, !!$preload );
106              
107 23         164 join '; ', "max-age=$max_age", ('includeSubDomains') x !!$include_subdomains, ('preload') x !!$preload;
108             }
109              
110             1;
111              
112             __END__