File Coverage

blib/lib/HTTP/State/Cookie.pm
Criterion Covered Total %
statement 134 159 84.2
branch 31 52 59.6
condition 26 53 49.0
subroutine 19 22 86.3
pod 5 6 83.3
total 215 292 73.6


line stmt bran cond sub pod time code
1 6     6   94793 use v5.36;
  6         35  
2              
3             package HTTP::State::Cookie;
4 6     6   35 no warnings "experimental";
  6         33  
  6         426  
5              
6             # Logging
7             #
8 6     6   1946 use Log::ger;
  6         82  
  6         50  
9 6     6   1923 use Log::OK;
  6         3341  
  6         43  
10              
11              
12              
13 6     6   95518 use builtin qw;
  6         808  
  6         2589  
14              
15              
16             my @months = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
17             my $i=0;
18             my %months= map {$_,$i++} @months;
19              
20             $i=0;
21             my @days= qw(Sun Mon Tue Wed Thu Fri Sat);
22             my %days= map {$_,$i++} @days;
23              
24             my @names;
25             my @same_site_names;
26             #my @values;
27             my %const_names;
28             my @pairs;
29             my @same_site_pairs;
30              
31             my %reverse;
32             my %same_site_reverse;
33             my @forward;
34              
35             BEGIN {
36 6     6   49 @names=qw<
37             Undef
38             Name
39             Value
40             Expires
41             Max-Age
42             Domain
43             Path
44             Secure
45             HttpOnly
46             SameSite
47             Partitioned
48            
49             Creation_Time
50             Last_Access_Time
51             Persistent
52             HostOnly
53             Key
54             >;
55              
56             #@values= 0 .. @names-1;
57              
58 6         2109 @same_site_names = qw;
59              
60 6         32 for my ($i)(0..$#names){
61 96         364 $const_names{("COOKIE_".uc $names[$i])=~tr/-/_/r}= $i;
62 96         2506 $reverse{lc $names[$i]}=$i;
63             }
64 6         18 $reverse{undef}=0; #catching
65              
66             #Additional keys in hash for HTTP::CookieJar support
67              
68              
69 6         20 for my ($i)(0..$#same_site_names){
70 30         70 $const_names{"SAME_SITE_".uc $same_site_names[$i]}=$i;
71 30         61 $same_site_reverse{lc $same_site_names[$i]}=$i;
72             }
73 6         378 $same_site_reverse{undef}=0; #catching
74             }
75              
76 6     6   46 use constant::more \%const_names;
  6         12  
  6         50  
77              
78              
79             use Export::These
80 6         185 "cookie_struct",
81             constants=>["cookie_struct", keys %const_names],
82             encode=>[qw],
83             decode=>[qw],
84             export_ok=>["hash_set_cookie"],
85             all=>
86 6     6   10609 [keys(%const_names),qw];
  6         7892  
87              
88              
89 6     6   15671 use Time::Local qw;
  6         27611  
  6         1401  
90              
91             my $tz_offset;
92              
93              
94             BEGIN {
95             # Low memory timezone offset calculation.
96             # Removes the need to load Time::Piece just for timezone offset
97 6     6   26 my $now=time;
98 6         26 my @g=gmtime $now;
99 6         121 my @l=localtime $now;
100 6         20 my $gs=$g[0]+ $g[1]*60 + $g[2]*3600 + $g[3]*86400;
101 6         12 my $ls=$l[0]+ $l[1]*60 + $l[2]*3600 + $l[3]*86400;
102 6         255 $tz_offset=$ls-$gs;
103             }
104              
105 6     6   45 use constant::more TZ_OFFSET=>$tz_offset;
  6         10  
  6         60  
106              
107              
108             # Expects the name and value as the first pair of arguments
109             sub cookie_struct {
110              
111 6     6   1048 no warnings "experimental";
  6         14  
  6         747  
112 26     26 1 596129 my @c=(1, shift, shift); # Reuse the first field as string/int marker
113              
114              
115 26 50       71 die "Cookie must have a name" unless $c[COOKIE_NAME];
116              
117 26 100       62 if(@_){
118 6     6   38 no warnings "uninitialized";
  6         11  
  6         232  
119 6     6   26 no warnings "numeric";
  6         12  
  6         2326  
120 25 50       89 if($c[$_[0]]){
121             # anticipate keys provided as string.
122             #
123             # If the first remaining argument is numeric (field constant) will be an undef value
124             # which when used in numeric constant will be 0. The $c[0] is set to one which is true
125             # which means we anticipate string names
126 25         46 for my ($k, $v)(@_){
127 48         140 $c[$reverse{lc $k}]=$v;
128             }
129             }
130             else{
131             # keys assumed to be integer constants
132             #
133 0         0 for my ($k, $v)(@_){
134 0         0 $c[$k]=$v;
135             }
136             }
137              
138              
139 25 100       57 $c[COOKIE_EXPIRES]-=TZ_OFFSET if defined $c[COOKIE_EXPIRES];
140 25 100       62 $c[COOKIE_DOMAIN]=scalar reverse lc $c[COOKIE_DOMAIN] if $c[COOKIE_DOMAIN];
141              
142 25         68 $c[COOKIE_SAMESITE]=$same_site_reverse{lc$c[COOKIE_SAMESITE]};
143 25   50     90 $c[COOKIE_HOSTONLY]//=0;
144              
145             }
146              
147 26   50     58 $c[COOKIE_NAME]//="";
148 26   100     85 $c[COOKIE_VALUE]//="";
149              
150             # Remove any extra fields added in haste
151             #
152             #splice @c, COOKIE_KEY+1;
153              
154 26         81 \@c;
155             }
156              
157              
158             # Supports a simple scalar or an array ref of simple scalars to parse/decode
159             sub decode_cookies {
160 6     6   45 no warnings "experimental";
  6         9  
  6         1523  
161 0 0   0 1 0 my @values= map trim($_), #trim leading /trailing white space
162             map split("=", $_, 2), #Split fields into KV pairs
163             split /;\s*/, ref($_[0])
164             ? join("; ", $_[0]->@*)
165             : $_[0]; #Split input into fields
166 0         0 @values;
167             }
168              
169             # Returns a newly created cookie struct from a Set-Cookie string. Does not
170             # validate or create default values of attributess. Only processes what is
171             # given
172             #
173             sub decode_set_cookie{
174 21 50   21 1 48 return undef unless $_[0];
175 6     6   56 no warnings "experimental";
  6         37  
  6         19773  
176             # $string, converter
177 21         30 my $input=$_[0];
178 21         39 my $key;
179             my $value;
180 21         0 my @values;
181 21         27 my $first=1;
182 21         36 my @fields;
183              
184             #Value needs to be the first field
185              
186 21         36 my $index=index $input, ";";
187 21         20 my $name_value;
188 21 50       39 if($index>=0){
189             # at least one ";" was found
190 21         31 $name_value=substr $input,0, $index;
191 21         32 substr $input, 0, $index+1, "";
192            
193             }
194             else {
195             # No ";" found
196 0         0 $name_value=$input;
197 0         0 $input="";
198             }
199            
200 21         23 Log::OK::TRACE and log_trace " decoding cookie name: name value: $name_value";
201              
202 21         56 $index=index $name_value, "=";
203              
204             #Abort unless has a name
205 21 50       38 return unless $index >0;
206              
207 21         40 $values[1]= substr $name_value, 0, $index;
208 21         38 $values[2]= substr $name_value, $index+1;
209 21         24 Log::OK::TRACE and log_trace " decoding cookie name: $values[1] value:$values[2]";
210              
211              
212             # trip whitespace
213 21         51 $values[1]=trim($values[1]);
214 21         34 $values[2]=trim($values[2]);
215              
216             # TODO: test for controll characters
217            
218              
219              
220 21         20 Log::OK::TRACE and log_trace " decoding cookie name: $values[1] value:$values[2]";
221              
222             #Process attributes if input remaining;
223 21 50       33 return \@values unless $input;
224              
225 21         109 @fields=split /;\s*/, $input;
226              
227 21         36 for(@fields){
228              
229 78         161 ($key, $value)=split "=", $_, 2;
230              
231 78         123 $key=trim($key);
232 78 100       143 $value=trim($value) if $value;
233              
234             # Attributes are processed with case insensitive names
235             #
236 78         145 $key=lc $key;
237              
238             # Look up the value key value pair
239             # unkown values are stored in the undef => 0 position
240 78   100     237 $values[$reverse{$key}]=$value//1;
241             }
242              
243             # nuke unkown value
244 21         30 $values[0]=undef;
245              
246              
247             # Fix the date. Date is stored in seconds internally
248             #
249 21   66     62 for($values[COOKIE_EXPIRES]//()){
250 10         27 Log::OK::TRACE and log_trace " converting cookie expires from stamp to epoch";
251 10         93 my ($wday_key, $mday, $mon_key, $year, $hour, $min, $sec, $tz)=
252             /([^,]+), (\d+).([^-]{3}).(\d{4}) (\d+):(\d+):(\d+) (\w+)/;
253             #TODO support parsing of other deprecated data formats
254              
255 10 50       41 if(70<=$year<=99){
    50          
256 0         0 $year+=1900;
257             }
258             elsif(0<=$year<=69){
259 0         0 $year+=2000;
260             }
261             else{
262             #year as is
263             }
264             #NOTE: timelocal_modern DOES NOT add/subtract time offset. Which is what we want
265             #as the time is already gmt
266             #
267 10         38 $_ = timelocal_modern($sec, $min, $hour, $mday, $months{$mon_key}, $year);
268             }
269              
270             # adjust creation and last modified times
271 21 100       786 if(defined $_[1]){
272 8 50       26 $values[COOKIE_CREATION_TIME]-=$_[1] if $values[COOKIE_CREATION_TIME];
273 8 50       48 $values[COOKIE_LAST_ACCESS_TIME]-=$_[1] if $values[COOKIE_LAST_ACCESS_TIME];
274              
275             }
276              
277              
278             # Fix leading/trailing dot
279 21   66     46 for($values[COOKIE_DOMAIN]//()){
280 11         22 s/\.$//;
281 11         17 s/^\.//;
282 11         23 $_ = scalar reverse $_;
283             }
284              
285             # Fix same site
286            
287 21   66     48 for($values[COOKIE_SAMESITE]//()){
288 12         29 $_=$same_site_reverse{lc $_};
289             }
290             # Ensure host only is defined
291 21   100     57 $values[COOKIE_HOSTONLY]//=0;
292              
293              
294            
295              
296 21         70 \@values;
297             }
298              
299             # Encodes KV pairs from supplied cookie structs
300             sub encode_cookies {
301 0   0 0 1 0 join "; ", map "$_->[COOKIE_NAME]=".($_->[COOKIE_VALUE]//""), @_;
302             }
303              
304             sub encode_set_cookie {
305 43     43 1 101 my ($cookie, $store_flag, $partition_key)=@_;
306 43         48 Log::OK::DEBUG and log_debug "Serializing set cookie";
307              
308             # Start with name and value
309             #
310 43   50     120 my $string= "$cookie->[COOKIE_NAME]=".($cookie->[COOKIE_VALUE]//"");
311              
312              
313              
314            
315             # Format date for expires. Internally the cookie structure stores this value
316             # in terms of GMT.
317             # Again only add the attribute if value is defined
318             #
319             #for($cookie->[COOKIE_PERSISTENT] &&
320 43   100     129 for($cookie->[COOKIE_EXPIRES]//()){
321             #
322             #NOTE: localtime doesn't add/subtract offsets. This is what we want as it was manually adjusted.
323             #
324 35         595 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) =localtime $_;
325 35         279 $string.="; $names[COOKIE_EXPIRES]=$days[$wday], ".sprintf("%02d",$mday)." $months[$mon] ".($year+1900) .sprintf(" %02d:%02d:%02d", $hour,$min,$sec)." GMT";
326             }
327             # Reverse the cookie domain (stored backwards) if preset. Don't add the attribute
328             # if not defined.
329             #
330             $string.= "; $names[COOKIE_DOMAIN]=".scalar reverse $_
331 43   100     167 for $cookie->[COOKIE_DOMAIN]//();
332              
333             # Do Attributes with needing values. Only add them if the attribute is
334             # defined
335             #
336 43         72 for my $index (COOKIE_MAX_AGE, COOKIE_PATH){
337 86   66     215 for($cookie->[$index]//()){
338 39         112 $string.="; $names[$index]=$_";
339             }
340             }
341              
342              
343             # Do flags (attibutes with no values)
344             #
345 43 100       88 $string.="; $names[COOKIE_SECURE]" if defined $cookie->[COOKIE_SECURE];
346 43 50       103 $string.="; $names[COOKIE_HTTPONLY]" if defined $cookie->[COOKIE_HTTPONLY];
347              
348 43 100       83 if(defined $store_flag){
349             # If asked for storage format, give internal values
350             #
351 33         57 $string.="; Creation_Time=".($cookie->[COOKIE_CREATION_TIME]+$store_flag);
352 33         49 $string.="; Last_Access_Time=".($cookie->[COOKIE_LAST_ACCESS_TIME]+$store_flag);
353 33 100       61 $string.="; HostOnly" if $cookie->[COOKIE_HOSTONLY];
354 33 100 100     134 $string.="; Partitioned=$partition_key" if $cookie->[COOKIE_PARTITIONED] and $partition_key; #Store the partition key in the partitioned field
355             #$string.="; Persistent" if $cookie->[COOKIE_PERSISTENT];
356             }
357 43 100       107 $string.="; $names[COOKIE_SAMESITE]=".$same_site_names[$cookie->[COOKIE_SAMESITE]] if $cookie->[COOKIE_SAMESITE];
358              
359 43         185 $string;
360              
361             }
362              
363             #mosty for compatibility with HTTP::CookieJar 'cookies_for' method
364             sub hash_set_cookie{
365 0     0 0   my ($cookie, $store_flag)=@_;
366 0           my %hash=(name=>$cookie->[COOKIE_NAME], value=>$cookie->[COOKIE_VALUE]);
367              
368             # Reverse the cookie domain (stored backwards) if preset. Don't add the attribute
369             # if not defined.
370             #
371             $hash{domain}=scalar reverse $_
372 0   0       for $cookie->[COOKIE_DOMAIN]//();
373              
374             # Do Attributes with needing values. Only add them if the attribute is
375             # defined
376             #
377 0   0       $hash{maxage}=$_ for $cookie->[COOKIE_MAX_AGE]//();
378 0   0       $hash{path}=$_ for $cookie->[COOKIE_PATH]//();
379 0   0       $hash{samesite}=$_ for $cookie->[COOKIE_SAMESITE]//();
380              
381            
382             # Format date for expires. Internally the cookie structure stores this value
383             # in terms of GMT.
384             # Again only add the attribute if value is defined
385             #
386 0   0       for($cookie->[COOKIE_PERSISTENT] && $cookie->[COOKIE_EXPIRES]//()){
      0        
387 0           my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) =localtime $_;
388 0           $hash{expires}="$days[$wday], ".sprintf("%02d",$mday)." $months[$mon] ".($year+1900) .sprintf(" %02d:%02d:%02d",$hour,$min,$sec)." GMT";
389             }
390              
391             # Do flags (attibutes with no values)
392             #
393 0 0         $hash{secure}=1 if defined $cookie->[COOKIE_SECURE];
394 0 0         $hash{httponly}=1 if defined $cookie->[COOKIE_HTTPONLY];
395              
396 0 0         if(defined $store_flag){
397             # If asked for storage format, give internal values
398             #
399 0 0         $hash{hostonly}=1 if $cookie->[COOKIE_HOSTONLY];
400 0           $hash{creation_time}=($cookie->[COOKIE_CREATION_TIME]+$store_flag);
401 0           $hash{access_time}=($cookie->[COOKIE_LAST_ACCESS_TIME]+$store_flag);
402             }
403              
404 0           \%hash;
405             }
406             1;