File Coverage

blib/lib/HTTP/State.pm
Criterion Covered Total %
statement 85 100 85.0
branch 13 28 46.4
condition 14 48 29.1
subroutine 14 15 93.3
pod 0 1 0.0
total 126 192 65.6


line stmt bran cond sub pod time code
1 6     6   583887 use v5.36;
  6         26  
2             package HTTP::State;
3              
4              
5             our $VERSION="v0.1.2";
6              
7             # Logging
8             #
9 6     6   11803 use Log::ger;
  6         333  
  6         45  
10 6     6   4133 use Log::OK;
  6         23084  
  6         49  
11              
12             # Object system. Will use feature class asap
13             #
14 6     6   14634 use Object::Pad;
  6         87898  
  6         37  
15              
16 6     6   3778 use HTTP::State::Cookie ":all";
  6         19  
  6         253  
17              
18              
19              
20             # Fast Binary Search subroutines
21             #
22 6     6   7962 use List::Insertion {type=>"string", duplicate=>"left", accessor=>"->[".COOKIE_KEY."]"};
  6         225750  
  6         109  
23              
24              
25             # Public suffix list loaded when needed
26             #
27             #use Mozilla::PublicSuffix qw;
28              
29             # Date
30             #use Time::Piece;
31             #my $tz_offset=Time::Piece->localtime->tzoffset->seconds;
32              
33             my $tz_offset=HTTP::State::Cookie::TZ_OFFSET;
34              
35             # Constant flags foor User agent context
36             #
37 6     6   30739 use constant::more FLAG_SAME_SITE=>0x01; # Indicate request is same-site/cross-site
  6         64  
  6         74  
38 6     6   1106 use constant::more FLAG_TYPE_HTTP=>0x02; # Indicate request is HTTP/non-HTTP
  6         11  
  6         34  
39 6     6   925 use constant::more FLAG_SAFE_METH=>0x04; # Indicate request is safe method
  6         11  
  6         21  
40 6     6   851 use constant::more FLAG_TOP_LEVEL=>0x04; # Indicate top level navigation
  6         11  
  6         125  
41              
42              
43              
44 6     6   833 use Export::These flags=>[qw];
  6         14  
  6         51  
45              
46              
47             class HTTP::State;
48              
49 6     6   10302 no warnings "experimental";
  6         10  
  6         75652  
50 0     0 0 0 field $_get_cookies_sub :reader; # Main cookie retrieval routine, reference.
  0         0  
51             field @_cookies; # An array of cookie 'structs', sorted by the COOKIE_KEY field
52             field %_partitions;
53             field $_suffix_cache;# :param=undef; #Hash ref used as cache
54             field $_public_suffix_sub :param=undef; # Sub used for public suffix lookup.
55             field $_second_level_domain_sub;
56             #field $_default_same_site :param="None";
57              
58             field $_lax_allowing_unsafe :param=undef;
59             field $_lax_allowing_unsafe_timeout :param=120;
60             field $_retrieve_sort :param=undef;
61              
62             field $_max_expiry :param=400*24*3600;
63              
64             field $_default_flags :param=FLAG_SAME_SITE|FLAG_TYPE_HTTP|FLAG_SAFE_METH;
65              
66              
67             field %_sld_cache;
68              
69             BUILD{
70             # Create the main lookup sub
71             $self->_make_get_cookies;
72             unless($_public_suffix_sub){
73             require Mozilla::PublicSuffix;
74             $_public_suffix_sub=\&Mozilla::PublicSuffix::public_suffix;
75             }
76             $_suffix_cache//={};
77             $_second_level_domain_sub=sub {
78             my $domain=$_[0];
79             my $highest="";
80             my $suffix=$_suffix_cache->{$domain}//=&$_public_suffix_sub;
81              
82              
83             if($suffix){
84             substr($domain, -(length($suffix)+1))="";
85              
86             if($domain){
87             my @labels=split /\./, $domain;
88             $highest=pop(@labels).".$suffix";
89             }
90             }
91             $highest;
92              
93             };
94             }
95            
96             sub _path_match {
97 7     7   19 my($path, $cookie)=@_;
98              
99             # Process path matching as per section 5.1.4 in RFC 6265
100             #
101 7   50     15 $path||="/"; #TODO find a reference to a standard or rfc for this
102 7         10 Log::OK::TRACE and log_trace "PATH: $path";
103 7         9 Log::OK::TRACE and log_trace "Cookie PATH: $_->[COOKIE_PATH]";
104 7         11 my $path_ok;
105 7 50       44 if($path eq $cookie->[COOKIE_PATH]){
    50          
    50          
106 0         0 $path_ok=1;
107             }
108              
109             elsif (substr($cookie->[COOKIE_PATH], -1, 1) eq "/"){
110             # Cookie path ends in a slash?
111 0         0 $path_ok=index($path, $cookie->[COOKIE_PATH])==0 # Yes, check if cookie path is a prefix
112             }
113             elsif(substr($path,length($cookie->[COOKIE_PATH]), 1) eq "/"){
114 7         38 $path_ok= 0==index $path, $cookie->[COOKIE_PATH];
115             }
116             else {
117             # Not a path match
118 0         0 $path_ok=undef;
119             }
120 7         9 Log::OK::TRACE and log_trace "Path ok: $path_ok";
121 7         83 $path_ok;
122             }
123              
124             #returns self for chaining
125              
126              
127             method store_cookies{
128             my ($request_uri, $partition_key, $flags, @cookies)=@_;
129             #TODO: fix this
130             $flags//=$_default_flags;
131             Log::OK::TRACE and log_trace __PACKAGE__. " store_cookies";
132             Log::OK::TRACE and log_trace __PACKAGE__. " ".join ", ", caller;
133             Log::OK::TRACE and log_trace __PACKAGE__. " $request_uri, $flags, @cookies";
134              
135             return $self unless @cookies;
136             # Parse the request_uri
137             #
138             #
139            
140             my $index;
141             my $host;
142             my $path;
143             my $authority;
144             my $scheme;
145              
146             $index=index $request_uri, "://";
147             $scheme=substr $request_uri, 0, $index, "";
148             substr($request_uri,0, 3)="";
149            
150             $index=index $request_uri, "/", $index;
151             if($index>0){
152             #have path
153             $authority=substr $request_uri, 0, $index, "";
154             $path=$request_uri;
155             }
156             else {
157             #no path
158             $authority=$request_uri;
159             $path="";
160             }
161              
162             # Parse out authority if username/password is provided
163             $index=index($authority, "@");
164             $authority= $index>0
165             ?substr $authority, $index+1
166             :$authority;
167              
168             # Find the host
169             $index=index $authority, ":";
170             $host=$index>0
171             ? substr $authority, 0, $index
172             : $authority;
173              
174             die "URI format error" unless $scheme and $host;
175              
176              
177              
178             my $time=time-$tz_offset; #Cache time. Translate to GMT
179              
180             # Iterate over the cookies supplied
181             SET_COOKIE_LOOP:
182             for my $c_ (@cookies){
183             # Parse or copy the input
184             my $c;
185             my $ref=ref $c_;
186             if($ref eq "ARRAY"){
187             # Assume a struct
188             $c=[@$c_]; #Copy
189             }
190             else {
191             # Assume a string
192             $c=decode_set_cookie($c_);
193             }
194             next unless $c;
195              
196              
197              
198              
199             #1.
200             # A user agent MAY ignore a received cookie in its entirety. See Section 5.3.
201              
202             #2.
203             # If cookie-name is empty and cookie-value is empty, abort these steps
204             # and ignore the cookie entirely.
205            
206             #3.
207             # If the cookie-name or the cookie-value contains a %x00-08 / %x0A-1F /
208             # %x7F character (CTL characters excluding HTAB), abort these steps and
209             # ignore the cookie entirely.
210              
211              
212             #4. If the sum of the lengths of cookie-name and cookie-value is more than
213             #4096 octets, abort these steps and ignore the cookie entirely
214             next if (length($c->[COOKIE_NAME])+ length($c->[COOKIE_VALUE]))>4096;
215             Log::OK::TRACE and log_trace __PACKAGE__. " Step 1, 2, 3, 4 OK";
216              
217             # 5. Create a new cookie with name cookie-name, value cookie-value. Set the
218             #creation-time and the last-access-time to the current date and time.
219             $c->[COOKIE_LAST_ACCESS_TIME]=$c->[COOKIE_CREATION_TIME]=$time;
220             Log::OK::TRACE and log_trace __PACKAGE__. " Step 5 OK";
221              
222             # 6.
223             # If the cookie-attribute-list contains an attribute with an attribute-name
224             # of "Max-Age":
225             # 1.
226             # Set the cookie's persistent-flag to true
227             #
228             # 2.
229             # Set the cookie's expiry-time
230             # to attribute-value of the last attribute in the cookie-attribute-list
231             # with an attribute-name of "Max-Age".
232             #
233             # Otherwise, if the cookie-attribute-list contains an attribute with an
234             # attribute-name of "Expires" (and does not contain an attribute with an
235             # attribute-name of "Max-Age"):
236             # 1.
237             # Set the cookie's persistent-flag to true.
238             #
239             # 2.
240             # Set the cookie's expiry-time to attribute-value of the last attribute
241             # in the cookie-attribute-list with an attribute-name of "Expires".
242             #
243             #Otherwise:
244             # 1.
245             # Set the cookie's persistent-flag to false.
246             #
247             # 2.
248             # Set the cookie's expiry-time to the latest representable date.
249              
250             if(defined $c->[COOKIE_MAX_AGE]){
251             $c->[COOKIE_PERSISTENT]=1;
252             $c->[COOKIE_EXPIRES]=$time+$c->[COOKIE_MAX_AGE];
253             Log::OK::TRACE and log_trace "max age set: $c->[COOKIE_MAX_AGE]";
254             }
255             elsif(defined $c->[COOKIE_EXPIRES]){
256             $c->[COOKIE_PERSISTENT]=1;
257             # expires already in required format
258             }
259             else{
260             $c->[COOKIE_PERSISTENT]=undef;
261             $c->[COOKIE_EXPIRES]=$time+$_max_expiry;
262             #400*24*3600; #Mimic chrome for maximum date
263              
264             }
265              
266             Log::OK::TRACE and log_trace "Expiry set to: $c->[COOKIE_EXPIRES]";
267             Log::OK::TRACE and log_trace __PACKAGE__. " Step 6 OK";
268              
269             #7.
270             #If the cookie-attribute-list contains an attribute with an attribute-name of "Domain":
271             #1.
272             #Let the domain-attribute be the attribute-value of the last attribute in
273             #the cookie-attribute-list with both an attribute-name of "Domain" and an
274             #attribute-value whose length is no more than 1024 octets. (Note that a
275             #leading %x2E ("."), if present, is ignored even though that character is
276             #not permitted.)
277             #
278             #Otherwise:
279             #1.
280             #Let the domain-attribute be the empty string.
281              
282             #8.
283             #If the domain-attribute contains a character that is not in the range of
284             #[USASCII] characters, abort these steps and ignore the cookie entirely.
285             #
286              
287             #9.
288             #If the user agent is configured to reject "public suffixes" and the
289             #domain-attribute is a public suffix:
290             #1.
291             #If the domain-attribute is identical to the canonicalized request-host:
292             #1.
293             #Let the domain-attribute be the empty string.
294             #Otherwise:
295             #1.
296             #Abort these steps and ignore the cookie entirely.
297              
298             #NOTE: This step prevents attacker.example from disrupting the integrity
299             #of site.example by setting a cookie with a Domain attribute of
300             #"example".
301              
302              
303              
304              
305              
306              
307              
308              
309             # Use the host as domain if none specified
310              
311             # Process the domain of the cookie. set to default if no explicitly set
312             #
313             my $rhost=scalar reverse $host;
314             my $sld;
315             my $suffix;
316             # DO a public suffix check on cookies. Need to ensure the domain for the cookie is NOT a suffix.
317             # This means we want a 'second level domain'
318             #
319             if($c->[COOKIE_DOMAIN]){
320             $suffix=$_suffix_cache->{$c->[COOKIE_DOMAIN]}//=scalar reverse
321             $_public_suffix_sub->(scalar reverse $c->[COOKIE_DOMAIN]);
322              
323             Log::OK::TRACE and log_trace "Looking up $c->[COOKIE_DOMAIN]=>$suffix";
324             if($suffix and $suffix eq $c->[COOKIE_DOMAIN]){
325             if($rhost eq $c->[COOKIE_DOMAIN]){
326              
327             Log::OK::TRACE and log_trace " Domain is equal to host, which is a suffix";
328             $c->[COOKIE_DOMAIN]="";
329             }
330             else {
331             Log::OK::TRACE and log_trace "Domain is public suffix. reject";
332             next;
333             }
334             }
335             }
336             Log::OK::TRACE and log_trace __PACKAGE__. " Step 7, 8, 9 OK";
337              
338             #10
339             #If the domain-attribute is non-empty:
340             #If the canonicalized request-host does not domain-match the domain-attribute:
341             #1.
342             #Abort these steps and ignore the cookie entirely.
343             #Otherwise:
344             #1
345             #Set the cookie's host-only-flag to false.
346             #2
347             #Set the cookie's domain to the domain-attribute.
348             #Otherwise:
349             #1
350             #Set the cookie's host-only-flag to true.
351             #2
352             #Set the cookie's domain to the canonicalized request-host.
353              
354              
355             if($c->[COOKIE_DOMAIN]){
356             if(0==index($rhost, $c->[COOKIE_DOMAIN])){
357             # Domain must be at least substring (parent domain).
358             $c->[COOKIE_HOSTONLY]=0;
359             }
360             else{
361             # Reject. no domain match
362             Log::OK::TRACE and log_trace __PACKAGE__."::store_cookie domain invalid";
363             next;
364             }
365             }
366             else{
367             Log::OK::TRACE and log_trace __PACKAGE__. " No domain set for cookie";
368             $c->[COOKIE_HOSTONLY]=1;
369             $c->[COOKIE_DOMAIN]=$rhost;
370             }
371              
372             Log::OK::TRACE and log_trace __PACKAGE__. " Step 10 OK";
373              
374             #11.
375             #If the cookie-attribute-list contains an attribute with an attribute-name
376             #of "Path", set the cookie's path to attribute-value of the last attribute
377             #in the cookie-attribute-list with both an attribute-name of "Path" and an
378             #attribute-value whose length is no more than 1024 octets. Otherwise, set
379             #the cookie's path to the default-path of the request-uri.
380              
381             $c->[COOKIE_PATH]//="";
382             next if length($c->[COOKIE_PATH])>1024;
383              
384             if( length($c->[COOKIE_PATH])==0 or substr($c->[COOKIE_PATH], 0, 1) ne "/"){
385             # Calculate default
386             if(length($path)==0 or substr($path, 0, 1 ) ne "/"){
387             $path="/";
388             }
389            
390             # Remove right / if present
391             if(length($path) >1){
392             my @parts=split "/", $path;
393             pop @parts;
394             $c->[COOKIE_PATH]=join "/", @parts;
395             }
396             else {
397             $c->[COOKIE_PATH]=$path;
398             }
399             }
400             Log::OK::TRACE and log_trace __PACKAGE__. " Step 11 OK";
401             #12
402             #If the cookie-attribute-list contains an attribute with an attribute-name
403             #of "Secure", set the cookie's secure-only-flag to true. Otherwise, set the
404             #cookie's secure-only-flag to false.
405              
406             #13
407             #If the scheme component of the request-uri does not denote a "secure"
408             #protocol (as defined by the user agent), and the cookie's secure-only-flag
409             #is true, then abort these steps and ignore the cookie entirely.
410              
411              
412             next if $c->[COOKIE_SECURE] and ($scheme ne "https");
413             Log::OK::TRACE and log_trace __PACKAGE__. " Step 12, 13 OK";
414              
415             #14
416             #If the cookie-attribute-list contains an attribute with an attribute-name
417             #of "HttpOnly", set the cookie's http-only-flag to true. Otherwise, set the
418             #cookie's http-only-flag to false.
419              
420             #15
421             #If the cookie was received from a "non-HTTP" API and the cookie's
422             #http-only-flag is true, abort these steps and ignore the cookie entirely.
423              
424             next if ($c->[COOKIE_HTTPONLY] and !($flags & FLAG_TYPE_HTTP));
425              
426              
427             Log::OK::TRACE and log_trace __PACKAGE__. " Step 14, 15 OK";
428              
429             #16
430             #If the cookie's secure-only-flag is false, and the scheme component of
431             #request-uri does not denote a "secure" protocol, then abort these steps
432             #and ignore the cookie entirely if the cookie store contains one or more
433             #cookies that meet all of the following criteria:
434             #
435             #1
436             #Their name matches the name of the newly-created cookie.
437             #2
438             #Their secure-only-flag is true.
439             #3
440             #Their domain domain-matches the domain of the newly-created cookie, or vice-versa.
441             #4
442             #The path of the newly-created cookie path-matches the path of the existing cookie.
443             #
444             #Note: The path comparison is not symmetric, ensuring only that a
445             #newly-created, non-secure cookie does not overlay an existing secure
446             #cookie, providing some mitigation against cookie-fixing attacks. That is,
447             #given an existing secure cookie named 'a' with a path of '/login', a
448             #non-secure cookie named 'a' could be set for a path of '/' or '/foo', but
449             #not for a path of '/login' or '/login/en'.
450              
451              
452             my $part;
453             if(!$c->[COOKIE_SECURE] and $scheme ne "https"){
454            
455             # get the second level domain to act as base to start search
456             $sld//=$_sld_cache{$c->[COOKIE_DOMAIN]}//=scalar reverse $_second_level_domain_sub->(scalar reverse $c->[COOKIE_DOMAIN]);
457             next unless defined $sld;
458              
459              
460             # IF partitions are enabled and the cookie is partitioned then lookup partition
461             # otherwise use normal cookies array
462             #
463             my @parts=(($partition_key and $c->[COOKIE_PARTITIONED])?$_partitions{$partition_key}//=[]: \@_cookies);
464             for my $part (@parts){
465             my $index=search_string_left $sld, $part;
466              
467             $index=@$part if $index<@$part and (index($part->[$index][COOKIE_KEY], $sld)==0);
468             my $found;
469             local $_;
470             while(!$found and $index<@$part){
471             $_=$part->[$index];
472             #exit the inner loop if the SLD is not a prefix of the current cookie key
473             last if index $_->[COOKIE_KEY], $sld;
474              
475             next SET_COOKIE_LOOP if $_->[COOKIE_SECURE]
476             and $_->[COOKIE_NAME] eq $c->[COOKIE_NAME] #name match
477             and (index($_->[COOKIE_DOMAIN], $sld)==0 or index($sld, $_->[COOKIE_DOMAIN])==0) # symmetric match
478             and _path_match $c->[COOKIE_PATH], $_; #path match
479              
480             $index++;
481             }
482             }
483             }
484             Log::OK::TRACE and log_trace __PACKAGE__. " Step 16 OK";
485              
486             #17
487             #If the cookie-attribute-list contains an attribute with an attribute-name
488             #of "SameSite", and an attribute-value of "Strict", "Lax", or "None", set
489             #the cookie's same-site-flag to the attribute-value of the last attribute
490             #in the cookie-attribute-list with an attribute-name of "SameSite".
491             #Otherwise, set the cookie's same-site-flag to "Default".
492              
493             $c->[COOKIE_SAMESITE]//=SAME_SITE_DEFAULT;#"Default";#$_default_same_site;
494              
495             Log::OK::TRACE and log_trace __PACKAGE__. " Step 17 OK";
496              
497             #18
498             #If the cookie's same-site-flag is not "None":
499             #1
500             #If the cookie was received from a "non-HTTP" API, and the API was called
501             #from a navigable's active document whose "site for cookies" is not
502             #same-site with the top-level origin, then abort these steps and ignore
503             #the newly created cookie entirely.
504             #2
505             #If the cookie was received from a "same-site" request (as defined in
506             #Section 5.2), skip the remaining substeps and continue processing the
507             #cookie.
508             #3
509             #If the cookie was received from a request which is navigating a
510             #top-level traversable [HTML] (e.g. if the request's "reserved client" is
511             #either null or an environment whose "target browsing context"'s
512             #navigable is a top-level traversable), skip the remaining substeps and
513             #continue processing the cookie.
514             #
515             #Note: Top-level navigations can create a cookie with any SameSite value,
516             #even if the new cookie wouldn't have been sent along with the request
517             #had it already existed prior to the navigation.
518             #4
519             #Abort these steps and ignore the newly created cookie entirely.
520              
521             if($c->[COOKIE_SAMESITE] != SAME_SITE_NONE){
522             if (not $flags & FLAG_TYPE_HTTP and not $flags & FLAG_SAME_SITE){
523             next;
524             }
525             elsif($flags & FLAG_SAME_SITE){
526             # continue
527             }
528             elsif($flags & FLAG_TOP_LEVEL){
529             # continue
530             }
531             else {
532             next;
533             }
534             }
535              
536             Log::OK::TRACE and log_trace __PACKAGE__. " Step 18 OK";
537              
538             #19
539             #If the cookie's "same-site-flag" is "None", abort these steps and ignore
540             #the cookie entirely unless the cookie's secure-only-flag is true.
541             next if $c->[COOKIE_SAMESITE] == SAME_SITE_NONE and !$c->[COOKIE_SECURE];
542              
543             Log::OK::TRACE and log_trace __PACKAGE__. " Step 19 OK";
544              
545             #20
546             #If the cookie-name begins with a case-insensitive match for the string
547             #"__Secure-", abort these steps and ignore the cookie entirely unless the
548             #cookie's secure-only-flag is true.
549             #
550             next if $c->[COOKIE_NAME]=~/^__Secure-/aai and !$c->[COOKIE_SECURE];
551              
552             Log::OK::TRACE and log_trace __PACKAGE__. " Step 20 OK";
553              
554             #21
555             #If the cookie-name begins with a case-insensitive match for the string
556             #"__Host-", abort these steps and ignore the cookie entirely unless the
557             #cookie meets all the following criteria:
558             #1
559             #The cookie's secure-only-flag is true.
560             #2
561             #The cookie's host-only-flag is true.
562             #3
563             #The cookie-attribute-list contains an attribute with an attribute-name
564             #of "Path", and the cookie's path is /.
565              
566             next if $c->[COOKIE_NAME]=~/^__Host-/aai and !($c->[COOKIE_SECURE] and
567             ($c->[COOKIE_PATH] eq "/") and $c->[COOKIE_HOSTONLY]);
568              
569             Log::OK::TRACE and log_trace __PACKAGE__. " Step 21 OK";
570              
571             #22
572             #If the cookie-name is empty and either of the following conditions are
573             #true, abort these steps and ignore the cookie:
574             #1
575             #the cookie-value begins with a case-insensitive match for the string
576             #"__Secure-"
577             #2
578             #the cookie-value begins with a case-insensitive match for the string
579             #"__Host-"
580             next if !$c->[COOKIE_NAME] and ($c->[COOKIE_VALUE]=~/^__Host-/i or $c->[COOKIE_VALUE]=~/^__Secure-/i);
581              
582             Log::OK::TRACE and log_trace __PACKAGE__. " Step 22 OK";
583            
584             #23
585             #If the cookie store contains a cookie with the same name, domain,
586             #host-only-flag, and path as the newly-created cookie:
587             #1
588             #Let old-cookie be the existing cookie with the same name, domain,
589             #host-only-flag, and path as the newly-created cookie. (Notice that this
590             #algorithm maintains the invariant that there is at most one such
591             #cookie.)
592             #2
593             #If the newly-created cookie was received from a "non-HTTP" API and the
594             #old-cookie's http-only-flag is true, abort these steps and ignore the
595             #newly created cookie entirely.
596             #3
597             #Update the creation-time of the newly-created cookie to match the creation-time of the old-cookie.
598             #4
599             #Remove the old-cookie from the cookie store.
600              
601             #24
602             #Insert the newly-created cookie into the cookie store.
603             #A cookie is "expired" if the cookie has an expiry date in the past.
604              
605             #The user agent MUST evict all expired cookies from the cookie store if, at
606             #any time, an expired cookie exists in the cookie store.
607            
608             #At any time, the user agent MAY "remove excess cookies" from the cookie
609             #store if the number of cookies sharing a domain field exceeds some
610             #implementation-defined upper bound (such as 50 cookies).
611              
612             #At any time, the user agent MAY "remove excess cookies" from the cookie
613             #store if the cookie store exceeds some predetermined upper bound (such as
614             #3000 cookies).
615              
616             #When the user agent removes excess cookies from the cookie store, the user
617             #agent MUST evict cookies in the following priority order:
618              
619             #1
620             #Expired cookies.
621             #2
622             #Cookies whose secure-only-flag is false, and which share a domain field
623             #with more than a predetermined number of other cookies.
624             #3
625             #Cookies that share a domain field with more than a predetermined number
626             #of other cookies.
627             #4
628             #All cookies.
629             #
630             #If two cookies have the same removal priority, the user agent MUST evict
631             #the cookie with the earliest last-access-time first.
632              
633             #When "the current session is over" (as defined by the user agent), the user
634             #agent MUST remove from the cookie store all cookies with the persistent-flag
635             #set to false.
636              
637              
638              
639              
640             # Build key to perform binary search in database. This key is unique in the database
641             #
642             $c->[COOKIE_KEY]="$c->[COOKIE_DOMAIN] $c->[COOKIE_PATH] $c->[COOKIE_NAME] $c->[COOKIE_HOSTONLY]";
643             $c->[COOKIE_MAX_AGE]=undef; # No longer need this, so
644             Log::OK::TRACE and log_trace __PACKAGE__."::store_cookie key: $c->[COOKIE_KEY]";
645              
646             # Locate the parition
647              
648             my @parts=(($partition_key and $c->[COOKIE_PARTITIONED])?$_partitions{$partition_key}//=[]: \@_cookies);
649              
650             for my $part (@parts){
651             # Lookup in database
652             #Index of left side insertion
653             my $index=search_string_left $c->[COOKIE_KEY], $part;
654             #say "insertion index: ", $index;
655             #say "Looking for cookie key: ".$c->[COOKIE_KEY];
656             #say " against cookie key: ".$part->[$index][COOKIE_KEY];
657              
658             #Test if actually found or just insertion point
659             my $found=($index<@$part and ($part->[$index][COOKIE_KEY] eq $c->[COOKIE_KEY]));
660              
661             if($found){
662             #reject if api call http only cookie currently exists
663             next if $part->[$index][COOKIE_HTTPONLY] and !($flags & FLAG_TYPE_HTTP);
664             $c->[COOKIE_CREATION_TIME]=$part->[$index][COOKIE_CREATION_TIME];
665             if($c->[COOKIE_EXPIRES]<=$time){
666             # Found but expired by new cookie. Delete the cookie
667             Log::OK::TRACE and log_trace __PACKAGE__. " found cookie and expired. purging";
668             splice @$part , $index, 1;
669             }
670             else {
671             # replace existing cookie
672             Log::OK::TRACE and log_trace __PACKAGE__. " found cookie. Updating";
673             $part->[$index]=$c;
674             }
675             }
676              
677             elsif($c->[COOKIE_EXPIRES]<$time){
678             Log::OK::TRACE and log_trace __PACKAGE__. " new cookie already expired. rejecting";
679             next; # new cookie already expired.
680             }
681             else {
682             # insert new cookie
683             Log::OK::TRACE and log_trace __PACKAGE__. " new cookie name. adding";
684             unless(@$part ){
685             push @$part , $c;
686             }
687             else{
688             #Log::OK::TRACE and log_trace __PACKAGE__. " new cookie name. adding";
689             splice @$part , $index, 0, $c;
690             }
691             }
692             }
693             Log::OK::TRACE and log_trace __PACKAGE__. " Step 23, 24 OK";
694             }
695             return $self;
696             }
697              
698              
699             method _make_get_cookies{
700             $_get_cookies_sub=sub {
701 4     4   16 my ($request_uri, $partition_key, $flags)=@_;
702 4   33     24 $flags//=$_default_flags;
703              
704 4         18 my $index;
705             my $host;
706 4         0 my $path;
707 4         0 my $authority;
708 4         0 my $scheme;
709              
710 4         9 $index=index $request_uri, "://";
711 4         11 $scheme=substr $request_uri, 0, $index, "";
712 4         11 substr($request_uri,0, 3)="";
713            
714 4         6 $index=index $request_uri, "/", $index;
715 4 50       9 if($index>0){
716             #have path
717 4         9 $authority=substr $request_uri, 0, $index, "";
718 4         7 $path=$request_uri;
719             }
720             else {
721             #no path
722 0         0 $authority=$request_uri;
723 0         0 $path="";
724             }
725              
726             # Parse out authority if username/password is provided
727 4         10 $index=index($authority, "@");
728 4 50       24 $authority= $index>0
729             ?substr $authority, $index+1
730             :$authority;
731              
732             # Find the host
733 4         9 $index=index $authority, ":";
734 4 50       9 $host=$index>0
735             ? substr $authority, 0, $index
736             : $authority;
737              
738 4 50 33     20 die "URI format error" unless $scheme and $host;
739              
740              
741             #:($host, undef)=split ":", $authority, 2;
742              
743             # Look up the second level domain. This will be the root for our domain search
744             #
745 4   100     18 my $sld=$_sld_cache{$host}//=scalar reverse $_second_level_domain_sub->($host);
746 4         10 my $rhost=scalar reverse $host;
747              
748              
749             # Iterate through all cookies until the domain no longer matches
750             #
751 4         11 my $time=time-$tz_offset;
752 4         6 my @output;
753              
754             # Search default cookies and also an existing parition. Don't create a new partition
755 4   66     30 my @parts=(\@_cookies, ($partition_key and $_partitions{$partition_key})||());
756             #########################################
757             # #my $part= #
758             # $partition_key #
759             # ? $_partitions{$partition_key}//=[] #
760             # : () #
761             # ); #
762             #########################################
763             #use Data::Dumper;
764             #say Dumper \@_cookies;
765 4         10 for my $part (@parts){
766              
767 7         281 $index=search_string_left $sld, $part;
768              
769 7         133 Log::OK::TRACE and log_trace __PACKAGE__. " index is: $index";
770 7         12 Log::OK::TRACE and log_trace "looking for host: $sld";
771              
772 7         11 local $_;
773              
774 7 50       18 $index++ unless @$part; # Force skip the test loop if no cookies in the jar
775              
776 7         17 while($index<@$part){
777 7         43 $_=$part->[$index];
778              
779             # End the search when the $sld of request is no longer a prefix for the
780             # cookie domain being tested
781 7 50       26 last if index $_->[COOKIE_DOMAIN], $sld;
782              
783             # Process expire. Do not update $index
784 7 50       18 if($_->[COOKIE_EXPIRES] <= $time){
785 0         0 Log::OK::TRACE and log_trace "cookie under test expired. removing";
786 0         0 splice @$part, $index, 1;
787 0         0 next;
788             }
789              
790              
791              
792              
793             ## At this point we have a domain match ##
794              
795             # Test for other restrictions...
796 7 50 0     31 $index++ and next if
      33        
      33        
      33        
      33        
      33        
      33        
797             (!_path_match($path, $_))
798             or ($_->[COOKIE_HOSTONLY] and $rhost ne $_->[COOKIE_DOMAIN])
799             or ($_->[COOKIE_SECURE] and $scheme ne "https")
800             or ($_->[COOKIE_HTTPONLY] and not $flags & FLAG_TYPE_HTTP);
801              
802 7 50 33     36 if((not ($flags & FLAG_SAME_SITE)) and ($_->[COOKIE_SAMESITE] != SAME_SITE_NONE)){
803              
804              
805              
806            
807 0   0     0 my $f=(($flags & FLAG_TYPE_HTTP)
808             and (($_->[COOKIE_SAMESITE] == SAME_SITE_LAX)
809             or ($_->[COOKIE_SAMESITE] == SAME_SITE_DEFAULT)
810             )
811             );
812 0   0     0 $f&&=(($flags & FLAG_SAFE_METH) or (
      0        
813             $_lax_allowing_unsafe and $_->[COOKIE_SAMESITE] == SAME_SITE_DEFAULT
814             and ($time-$_->[COOKIE_CREATION_TIME]) < $_lax_allowing_unsafe_timeout
815             ));
816              
817 0   0     0 $f&&=($flags & FLAG_TOP_LEVEL);
818             }
819              
820             #
821             # If we get here, cookie should be included!
822             #Update last access time
823             #
824 7         15 $_->[COOKIE_LAST_ACCESS_TIME]=$time;
825 7         10 Log::OK::TRACE and log_trace "Pushing cookie";
826 7         18 push @output, $_;
827 7         25 $index++;
828             }
829             }
830            
831             # TODO:
832             # Sort the output as recommended by RFC 6525
833             # The user agent SHOULD sort the cookie-list in the following
834             # order:
835             #
836             # * Cookies with longer paths are listed before cookies with
837             # shorter paths.
838             #
839             # * Among cookies that have equal-length path fields, cookies with
840             # earlier creation-times are listed before cookies with later
841             # creation-times.
842              
843 4 50       10 if($_retrieve_sort ){
844             @output= sort {
845 0 0       0 length($b->[COOKIE_PATH]) <=> length($a->[COOKIE_PATH])
  0         0  
846             || $a->[COOKIE_CREATION_TIME] <=> $b->[COOKIE_CREATION_TIME]
847             } @output;
848             }
849 4         129 \@output;
850             };
851             }
852              
853             method get_cookies{
854             # Do a copy of the matching entries
855             #
856             map [@$_], $_get_cookies_sub->&*->@*;
857             }
858              
859              
860             #TODO rename to retrieve_cookies?
861             method retrieve_cookies{
862             my $cookies=&$_get_cookies_sub;
863             join "; ", map "$_->[COOKIE_NAME]=$_->[COOKIE_VALUE]", @$cookies;
864             }
865             #*retrieve_cookies=\&encode_request_cookies;
866              
867             method get_kv_cookies{#
868             my $cookies=&$_get_cookies_sub;
869             map(($_->[COOKIE_NAME], $_->[COOKIE_VALUE]), @$cookies);
870             }
871              
872              
873             method db {
874             \@_cookies;
875             }
876              
877              
878              
879              
880              
881             # Compatibility matrix
882             # HTTP::CookieJar
883             # Additional api
884             # new
885             # create a new jar
886             # clear
887             # empty the jar
888             # dump_cookies
889             #
890             #
891             # Used by:
892             # HTTP::Tiny
893             # FURL
894             # Expected API
895             # $jar->add($url, $set_cookie_string)
896             # Parse set cookie string and add cookie to jar
897             #
898             # #jar->cookie_header($url)
899             # Retrieve cookies from jar and serialize for header
900             #
901             #
902             # Returns self for chaining
903             method clear{
904             @_cookies=(); #Clear the db
905             %_partitions=();
906             $self;
907             }
908             method add {
909             $self->store_cookies(shift, undef, $_default_flags, @_);
910             }
911              
912             method cookie_header {
913             splice @_, 1, 0, undef, $_default_flags;
914             my $cookies=&$_get_cookies_sub;
915             }
916              
917             method dump_cookies {
918             my $all=$_[0]?!$_[0]{persistent}:1;
919             my @out=map encode_set_cookie($_, $tz_offset) , grep $_->[COOKIE_PERSISTENT]||$all, @_cookies;
920             for my ($k)(sort keys %_partitions){
921             my $v=$_partitions{$k};
922             push @out, map encode_set_cookie($_, $tz_offset, $k) , grep $_->[COOKIE_PERSISTENT]||$all, @$v;
923             }
924             @out;
925             }
926              
927             method cookies_for{
928             my $cookies=&$_get_cookies_sub;
929             map hash_set_cookie($_,1), @$cookies;
930             }
931              
932             #TODO: add test for bulk adding of strings and structs
933             method load_cookies{
934             my $index;
935             my $time=time-$tz_offset;
936             my $c;
937             for my $s (@_){
938              
939             Log::OK::TRACE and log_trace "+++";
940             Log::OK::TRACE and log_trace "loading cookie from string";
941             Log::OK::TRACE and log_trace $s;
942             next unless $c=decode_set_cookie($s, $tz_offset);
943             # Don't load if cookie is expired
944             #
945             next if $c->[COOKIE_EXPIRES]<=$time;
946              
947             # Build key for search
948             $c->[COOKIE_KEY]="$c->[COOKIE_DOMAIN] $c->[COOKIE_PATH] $c->[COOKIE_NAME] $c->[COOKIE_HOSTONLY]";
949              
950              
951             # Adjust the partitioned flag
952             my $partition_key=$c->[COOKIE_PARTITIONED];
953              
954             my $part=\@_cookies; #default is the unparitioned jar
955              
956             if($partition_key){
957             $c->[COOKIE_PARTITIONED]=1;
958             $part=$_partitions{$partition_key}//=[];
959             }
960              
961             # update the list
962             unless(@$part){
963             Log::OK::TRACE and log_trace "Pushing cookie in to empty jar/parition";
964             push @$part, $c;
965             }
966             else{
967             # Do binary search
968             #
969             $index=search_string_left $c->[COOKIE_KEY], $part;#\@_cookies;
970             # If the key is identical, then we prefer the latest cookie,
971             # TODO: Fix key with scheme?
972             if($index<@$part and ($part->[$index][COOKIE_KEY] eq $c->[COOKIE_KEY])){
973             Log::OK::TRACE and log_trace "replace cookie in jar/parition";
974             $part->[$index]=$c;
975             }
976             else {
977             Log::OK::TRACE and log_trace "splicing cookie in to jar/parition";
978             splice @$part, $index,1,$c;
979             }
980              
981              
982             #splice @$part, $index, $replace, $c;
983             }
984             }
985             }
986              
987             1;