File Coverage

blib/lib/PAGI/Middleware/Cookie.pm
Criterion Covered Total %
statement 68 75 90.6
branch 12 22 54.5
condition 10 16 62.5
subroutine 15 16 93.7
pod 1 1 100.0
total 106 130 81.5


line stmt bran cond sub pod time code
1             package PAGI::Middleware::Cookie;
2              
3 1     1   207548 use strict;
  1         2  
  1         35  
4 1     1   9 use warnings;
  1         1  
  1         42  
5 1     1   341 use parent 'PAGI::Middleware';
  1         275  
  1         4  
6 1     1   51 use Future::AsyncAwait;
  1         2  
  1         4  
7 1     1   384 use Cookie::Baker ();
  1         3602  
  1         804  
8              
9             =head1 NAME
10              
11             PAGI::Middleware::Cookie - Cookie parsing middleware
12              
13             =head1 SYNOPSIS
14              
15             use PAGI::Middleware::Builder;
16              
17             my $app = builder {
18             enable 'Cookie';
19             $my_app;
20             };
21              
22             # In your app:
23             async sub app {
24             my ($scope, $receive, $send) = @_;
25              
26             my $cookies = $scope->{'pagi.cookies'};
27             my $session_id = $cookies->{session_id};
28             }
29              
30             =head1 DESCRIPTION
31              
32             PAGI::Middleware::Cookie parses the Cookie header and makes the parsed
33             cookies available in C<< $scope->{'pagi.cookies'} >> as a hashref.
34              
35             It also provides a helper for setting response cookies.
36              
37             =head1 CONFIGURATION
38              
39             =over 4
40              
41             =item * secret (optional)
42              
43             Secret key for signed cookies. Required for C/C.
44              
45             =back
46              
47             =cut
48              
49             sub _init {
50 2     2   4 my ($self, $config) = @_;
51              
52 2         6 $self->{secret} = $config->{secret};
53             }
54              
55             sub wrap {
56 2     2 1 19 my ($self, $app) = @_;
57              
58 2     2   46 return async sub {
59 2         5 my ($scope, $receive, $send) = @_;
60 2 50       6 if ($scope->{type} ne 'http') {
61 0         0 await $app->($scope, $receive, $send);
62 0         0 return;
63             }
64              
65             # Parse cookies from Cookie header
66 2   100     16 my $cookie_header = $self->_get_header($scope, 'cookie') // '';
67 2         5 my $cookies = $self->_parse_cookies($cookie_header);
68              
69             # Create cookie jar for setting response cookies
70 2         60 my @response_cookies;
71             my $cookie_jar = PAGI::Middleware::Cookie::Jar->new(
72             \@response_cookies,
73 1         3 sub { $self->_format_set_cookie(@_) },
74 2         25 );
75              
76             # Add cookies and jar to scope
77 2         10 my $new_scope = {
78             %$scope,
79             'pagi.cookies' => $cookies,
80             'pagi.cookie_jar' => $cookie_jar,
81             };
82              
83             # Wrap send to add Set-Cookie headers
84 4         277 my $wrapped_send = async sub {
85 4         6 my ($event) = @_;
86 4 100 100     14 if ($event->{type} eq 'http.response.start' && @response_cookies) {
87 1   50     2 my @headers = @{$event->{headers} // []};
  1         3  
88 1         1 for my $cookie (@response_cookies) {
89 1         3 push @headers, ['Set-Cookie', $cookie];
90             }
91             await $send->({
92             %$event,
93             headers => \@headers,
94 1         4 });
95             } else {
96 3         7 await $send->($event);
97             }
98 2         20 };
99              
100 2         6 await $app->($new_scope, $receive, $wrapped_send);
101 2         11 };
102             }
103              
104             sub _parse_cookies {
105 2     2   4 my ($self, $header) = @_;
106              
107 2 100 66     10 return {} unless defined $header && length $header;
108 1         4 return Cookie::Baker::crush_cookie($header);
109             }
110              
111             sub _format_set_cookie {
112 1     1   3 my ($self, $name, $value, %opts) = @_;
113              
114             my %cookie_opts = (
115             value => $value,
116 1   50     6 path => $opts{path} // '/',
117             );
118 1 50       4 $cookie_opts{domain} = $opts{domain} if defined $opts{domain};
119 1 50       3 $cookie_opts{expires} = $opts{expires} if defined $opts{expires};
120 1 50       3 $cookie_opts{'max-age'} = $opts{max_age} if defined $opts{max_age};
121 1 50       3 $cookie_opts{secure} = $opts{secure} if $opts{secure};
122 1 50       4 $cookie_opts{httponly} = $opts{httponly} if $opts{httponly};
123 1 50       3 $cookie_opts{samesite} = $opts{samesite} if defined $opts{samesite};
124 1         3 return Cookie::Baker::bake_cookie($name, \%cookie_opts);
125             }
126              
127             sub _get_header {
128 2     2   5 my ($self, $scope, $name) = @_;
129              
130 2         5 $name = lc($name);
131 2   50     3 for my $h (@{$scope->{headers} // []}) {
  2         7  
132 1 50       5 return $h->[1] if lc($h->[0]) eq $name;
133             }
134 1         5 return;
135             }
136              
137             # Simple cookie jar class for method-style access
138             package PAGI::Middleware::Cookie::Jar;
139              
140 1     1   6 use strict;
  1         1  
  1         25  
141 1     1   3 use warnings;
  1         1  
  1         294  
142              
143             sub new {
144 2     2   4 my ($class, $cookies_ref, $formatter) = @_;
145              
146 2         5 return bless {
147             cookies => $cookies_ref,
148             formatter => $formatter,
149             }, $class;
150             }
151              
152             sub set {
153 1     1   8 my ($self, $name, $value, %opts) = @_;
154              
155 1         2 push @{$self->{cookies}}, $self->{formatter}->($name, $value, %opts);
  1         7  
156             }
157              
158             sub delete {
159 0     0     my ($self, $name, %opts) = @_;
160              
161             my %cookie_opts = (
162             value => '',
163             expires => 0, # Epoch 0 = expired
164 0   0       path => $opts{path} // '/',
165             );
166 0 0         $cookie_opts{domain} = $opts{domain} if defined $opts{domain};
167 0           push @{$self->{cookies}}, Cookie::Baker::bake_cookie($name, \%cookie_opts);
  0            
168             }
169              
170             package PAGI::Middleware::Cookie;
171              
172             1;
173              
174             __END__