File Coverage

blib/lib/PAGI/Middleware/Session/State/Cookie.pm
Criterion Covered Total %
statement 53 53 100.0
branch 12 16 75.0
condition 11 17 64.7
subroutine 10 10 100.0
pod 4 4 100.0
total 90 100 90.0


line stmt bran cond sub pod time code
1             package PAGI::Middleware::Session::State::Cookie;
2              
3 3     3   403 use strict;
  3         5  
  3         93  
4 3     3   11 use warnings;
  3         3  
  3         135  
5 3     3   333 use parent 'PAGI::Middleware::Session::State';
  3         282  
  3         17  
6              
7             =head1 NAME
8              
9             PAGI::Middleware::Session::State::Cookie - Cookie-based session ID transport
10              
11             =head1 SYNOPSIS
12              
13             use PAGI::Middleware::Session::State::Cookie;
14              
15             my $state = PAGI::Middleware::Session::State::Cookie->new(
16             cookie_name => 'pagi_session',
17             cookie_options => { httponly => 1, path => '/', samesite => 'Lax' },
18             expire => 3600,
19             );
20              
21             # Extract session ID from request
22             my $id = $state->extract($scope);
23              
24             # Inject Set-Cookie header into response
25             $state->inject(\@headers, $id, {});
26              
27             =head1 DESCRIPTION
28              
29             Implements the L interface using HTTP
30             cookies for session ID transport. The session ID is read from the Cookie
31             request header and set via the Set-Cookie response header.
32              
33             =head1 CONFIGURATION
34              
35             =over 4
36              
37             =item * cookie_name (default: 'pagi_session')
38              
39             Name of the cookie used to store the session ID.
40              
41             =item * cookie_options (default: { httponly => 1, path => '/', samesite => 'Lax' })
42              
43             Cookie attributes applied when setting the response cookie.
44              
45             =item * expire (default: 3600)
46              
47             Max-Age value for the session cookie, in seconds.
48              
49             =back
50              
51             =cut
52              
53             sub new {
54 26     26 1 32898 my ($class, %options) = @_;
55              
56 26   100     801 $options{cookie_name} //= 'pagi_session';
57             $options{cookie_options} //= {
58 26   100     102 httponly => 1,
59             path => '/',
60             samesite => 'Lax',
61             };
62 26   100     69 $options{expire} //= 3600;
63              
64 26         101 return $class->SUPER::new(%options);
65             }
66              
67             =head2 extract
68              
69             my $session_id = $state->extract($scope);
70              
71             Find the Cookie header in C<$scope-E{headers}> (case-insensitive),
72             parse the cookie string, and return the value matching C.
73             Returns undef if no matching cookie is found.
74              
75             =cut
76              
77             sub extract {
78 20     20 1 62 my ($self, $scope) = @_;
79              
80 20         36 my $cookie_header = $self->_get_header($scope, 'cookie');
81 20 100       50 return unless defined $cookie_header;
82              
83 10         19 my $cookies = $self->_parse_cookies($cookie_header);
84 10         43 return $cookies->{$self->{cookie_name}};
85             }
86              
87             =head2 inject
88              
89             $state->inject(\@headers, $id, \%options);
90              
91             Format a Set-Cookie string and push C<['Set-Cookie', $cookie_string]>
92             onto the provided headers arrayref.
93              
94             =cut
95              
96             sub inject {
97 16     16 1 52 my ($self, $headers, $id, $options) = @_;
98              
99 16         70 my $cookie = $self->_format_cookie($id);
100 16         46 push @$headers, ['Set-Cookie', $cookie];
101             }
102              
103             =head2 clear
104              
105             $state->clear(\@headers);
106              
107             Expire the session cookie by pushing a Set-Cookie header with
108             C. Called when a session is destroyed.
109              
110             =cut
111              
112             sub clear {
113 5     5 1 21 my ($self, $headers) = @_;
114 5   50     19 my $cookie = "$self->{cookie_name}=; Path=" . ($self->{cookie_options}{path} // '/') . "; Max-Age=0";
115 5 100       13 $cookie .= "; HttpOnly" if $self->{cookie_options}{httponly};
116 5         13 push @$headers, ['Set-Cookie', $cookie];
117             }
118              
119             sub _get_header {
120 20     20   37 my ($self, $scope, $name) = @_;
121              
122 20         31 $name = lc($name);
123 20   50     22 for my $h (@{$scope->{headers} // []}) {
  20         71  
124 11 100       39 return $h->[1] if lc($h->[0]) eq $name;
125             }
126 10         18 return;
127             }
128              
129             sub _parse_cookies {
130 10     10   15 my ($self, $header) = @_;
131              
132 10         14 my %cookies;
133 10         45 for my $pair (split /\s*;\s*/, $header) {
134 13         33 my ($name, $value) = split /=/, $pair, 2;
135 13 50 33     44 next unless defined $name && $name ne '';
136 13         36 $name =~ s/^\s+//;
137 13         24 $name =~ s/\s+$//;
138 13   50     20 $value //= '';
139 13         22 $value =~ s/^\s+//;
140 13         34 $value =~ s/\s+$//;
141 13         48 $cookies{$name} = $value;
142             }
143 10         24 return \%cookies;
144             }
145              
146             sub _format_cookie {
147 16     16   27 my ($self, $session_id) = @_;
148              
149 16         44 my $cookie = "$self->{cookie_name}=$session_id";
150 16         19 my $opts = $self->{cookie_options};
151              
152 16   50     42 $cookie .= "; Path=" . ($opts->{path} // '/');
153 16 50       30 $cookie .= "; HttpOnly" if $opts->{httponly};
154 16 100       33 $cookie .= "; Secure" if $opts->{secure};
155 16 50       39 $cookie .= "; SameSite=$opts->{samesite}" if $opts->{samesite};
156 16 50       38 $cookie .= "; Max-Age=$self->{expire}" if $self->{expire};
157              
158 16         37 return $cookie;
159             }
160              
161             1;
162              
163             __END__