11/*
2- * Copyright (c) 2005, 2023 , Oracle and/or its affiliates. All rights reserved.
2+ * Copyright (c) 2005, 2025 , Oracle and/or its affiliates. All rights reserved.
33 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44 *
55 * This code is free software; you can redistribute it and/or modify it
5151 * <i>http://www.ietf.org/rfc/rfc2965.txt</i></a>
5252 * </blockquote>
5353 *
54- * <p> HttpCookie class can accept all these 3 forms of syntax.
54+ * <p> HttpCookie class can accept all these 3 forms of syntax. This class also provides
55+ * partial support for RFC 6265.
5556 *
5657 * @spec https://www.rfc-editor.org/info/rfc2109 RFC 2109: HTTP State Management Mechanism
5758 * @spec https://www.rfc-editor.org/info/rfc2965 RFC 2965: HTTP State Management Mechanism
59+ * @spec https://www.rfc-editor.org/info/rfc6265 RFC 6265: HTTP State Management Mechanism
5860 * @author Edward Wang
5961 * @since 1.6
6062 */
@@ -185,14 +187,14 @@ private HttpCookie(String name, String value, String header) {
185187 * if the header string is {@code null}
186188 */
187189 public static List <HttpCookie > parse (String header ) {
188- return parse (header , false );
190+ return parse (header , false , - 1L );
189191 }
190192
191193 // Private version of parse() that will store the original header used to
192194 // create the cookie, in the cookie itself. This can be useful for filtering
193195 // Set-Cookie[2] headers, using the internal parsing logic defined in this
194- // class.
195- private static List <HttpCookie > parse (String header , boolean retainHeader ) {
196+ // class. Also, allows for testing by specifying the creation time.
197+ static List <HttpCookie > parse (String header , boolean retainHeader , long currentTimeMillis ) {
196198
197199 int version = guessCookieVersion (header );
198200
@@ -209,7 +211,7 @@ private static List<HttpCookie> parse(String header, boolean retainHeader) {
209211 // so the parse logic is slightly different
210212 if (version == 0 ) {
211213 // Netscape draft cookie
212- HttpCookie cookie = parseInternal (header , retainHeader );
214+ HttpCookie cookie = parseInternal (header , retainHeader , currentTimeMillis );
213215 cookie .setVersion (0 );
214216 cookies .add (cookie );
215217 } else {
@@ -218,7 +220,7 @@ private static List<HttpCookie> parse(String header, boolean retainHeader) {
218220 // it'll separate them with comma
219221 List <String > cookieStrings = splitMultiCookies (header );
220222 for (String cookieStr : cookieStrings ) {
221- HttpCookie cookie = parseInternal (cookieStr , retainHeader );
223+ HttpCookie cookie = parseInternal (cookieStr , retainHeader , currentTimeMillis );
222224 cookie .setVersion (1 );
223225 cookies .add (cookie );
224226 }
@@ -230,20 +232,27 @@ private static List<HttpCookie> parse(String header, boolean retainHeader) {
230232 // ---------------- Public operations --------------
231233
232234 /**
233- * Reports whether this HTTP cookie has expired or not.
235+ * Reports whether this HTTP cookie has expired or not. This is
236+ * based on whether {@link #getMaxAge()} seconds have elapsed since
237+ * this object was created.
234238 *
235239 * @return {@code true} to indicate this HTTP cookie has expired;
236240 * otherwise, {@code false}
237241 */
238242 public boolean hasExpired () {
243+ return hasExpired (System .currentTimeMillis ());
244+ }
245+
246+ // PP for testing
247+ boolean hasExpired (long currentTimeMillis ) {
239248 if (maxAge == 0 ) return true ;
240249
241250 // if not specify max-age, this cookie should be
242251 // discarded when user agent is to be closed, but
243252 // it is not expired.
244253 if (maxAge < 0 ) return false ;
245254
246- long deltaSecond = (System . currentTimeMillis () - whenCreated ) / 1000 ;
255+ long deltaSecond = (currentTimeMillis - whenCreated ) / 1000 ;
247256 if (deltaSecond > maxAge )
248257 return true ;
249258 else
@@ -411,8 +420,19 @@ public void setMaxAge(long expiry) {
411420 }
412421
413422 /**
414- * Returns the maximum age of the cookie, specified in seconds. By default,
415- * {@code -1} indicating the cookie will persist until browser shutdown.
423+ * Returns the maximum age of the cookie, specified in seconds from the time
424+ * the object was created. By default, {@code -1} indicating the cookie will
425+ * persist until browser shutdown.
426+ *
427+ * The value of this attribute is determined by the following steps, in line
428+ * with RFC 6265:
429+ *
430+ * <ol><li>If {@link #setMaxAge(long)} was called, return the value set.</li>
431+ * <li>If previous step failed, and a {@code Max-Age} attribute was parsed
432+ * then return that value.</li>
433+ * <li>If previous step failed, and an {@code Expires} attribute was parsed
434+ * then the maxAge calculated at parsing time from that date, is returned</li>
435+ * <li>If previous step failed, then return {@code -1}.</li></ol>
416436 *
417437 * @return an integer specifying the maximum age of the cookie in seconds
418438 *
@@ -810,10 +830,13 @@ private static boolean isToken(String value) {
810830 * if header string violates the cookie specification
811831 */
812832 private static HttpCookie parseInternal (String header ,
813- boolean retainHeader )
833+ boolean retainHeader ,
834+ long currentTimeMillis )
814835 {
815836 HttpCookie cookie = null ;
816837 String namevaluePair = null ;
838+ if (currentTimeMillis == -1L )
839+ currentTimeMillis = System .currentTimeMillis ();
817840
818841 StringTokenizer tokenizer = new StringTokenizer (header , ";" );
819842
@@ -828,10 +851,11 @@ private static HttpCookie parseInternal(String header,
828851 if (retainHeader )
829852 cookie = new HttpCookie (name ,
830853 stripOffSurroundingQuote (value ),
831- header );
854+ header , currentTimeMillis );
832855 else
833856 cookie = new HttpCookie (name ,
834- stripOffSurroundingQuote (value ));
857+ stripOffSurroundingQuote (value ),
858+ null , currentTimeMillis );
835859 } else {
836860 // no "=" in name-value pair; it's an error
837861 throw new IllegalArgumentException ("Invalid cookie name-value pair" );
@@ -840,6 +864,10 @@ private static HttpCookie parseInternal(String header,
840864 throw new IllegalArgumentException ("Empty cookie header string" );
841865 }
842866
867+ // Attributes that require special handling
868+ String expiresValue = null ;
869+ String maxAgeValue = null ;
870+
843871 // remaining name-value pairs are cookie's attributes
844872 while (tokenizer .hasMoreTokens ()) {
845873 namevaluePair = tokenizer .nextToken ();
@@ -852,10 +880,19 @@ private static HttpCookie parseInternal(String header,
852880 name = namevaluePair .trim ();
853881 value = null ;
854882 }
883+ if (name .equalsIgnoreCase ("max-age" ) && maxAgeValue == null ) {
884+ maxAgeValue = value ;
885+ continue ;
886+ }
887+ if (name .equalsIgnoreCase ("expires" ) && expiresValue == null ) {
888+ expiresValue = value ;
889+ continue ;
890+ }
855891
856892 // assign attribute to cookie
857893 assignAttribute (cookie , name , value );
858894 }
895+ assignMaxAgeAttribute (cookie , expiresValue , maxAgeValue );
859896
860897 return cookie ;
861898 }
@@ -903,20 +940,6 @@ public void assign(HttpCookie cookie,
903940 cookie .setDomain (attrValue );
904941 }
905942 });
906- assignors .put ("max-age" , new CookieAttributeAssignor (){
907- public void assign (HttpCookie cookie ,
908- String attrName ,
909- String attrValue ) {
910- try {
911- long maxage = Long .parseLong (attrValue );
912- if (cookie .getMaxAge () == MAX_AGE_UNSPECIFIED )
913- cookie .setMaxAge (maxage );
914- } catch (NumberFormatException ignored ) {
915- throw new IllegalArgumentException (
916- "Illegal cookie max-age attribute" );
917- }
918- }
919- });
920943 assignors .put ("path" , new CookieAttributeAssignor (){
921944 public void assign (HttpCookie cookie ,
922945 String attrName ,
@@ -959,17 +982,37 @@ public void assign(HttpCookie cookie,
959982 }
960983 }
961984 });
962- assignors .put ("expires" , new CookieAttributeAssignor (){ // Netscape only
963- public void assign (HttpCookie cookie ,
964- String attrName ,
965- String attrValue ) {
966- if (cookie .getMaxAge () == MAX_AGE_UNSPECIFIED ) {
967- long delta = cookie .expiryDate2DeltaSeconds (attrValue );
968- cookie .setMaxAge (delta > 0 ? delta : 0 );
969- }
970- }
971- });
972985 }
986+
987+ private static void assignMaxAgeAttribute (HttpCookie cookie ,
988+ String expiresValue ,
989+ String maxAgeValue )
990+ {
991+ if (cookie .getMaxAge () != MAX_AGE_UNSPECIFIED )
992+ return ;
993+ if (expiresValue == null && maxAgeValue == null )
994+ return ;
995+
996+ // strip off the surrounding "-sign if there's any
997+ expiresValue = stripOffSurroundingQuote (expiresValue );
998+ maxAgeValue = stripOffSurroundingQuote (maxAgeValue );
999+
1000+ try {
1001+ if (maxAgeValue != null ) {
1002+ long maxAge = Long .parseLong (maxAgeValue );
1003+ cookie .maxAge = maxAge ;
1004+ return ;
1005+ }
1006+ } catch (NumberFormatException ignored ) {}
1007+
1008+ try {
1009+ if (expiresValue != null ) {
1010+ long delta = cookie .expiryDate2DeltaSeconds (expiresValue );
1011+ cookie .maxAge = (delta > 0 ? delta : 0 );
1012+ }
1013+ } catch (NumberFormatException ignored ) {}
1014+ }
1015+
9731016 private static void assignAttribute (HttpCookie cookie ,
9741017 String attrName ,
9751018 String attrValue )
@@ -989,7 +1032,7 @@ private static void assignAttribute(HttpCookie cookie,
9891032 SharedSecrets .setJavaNetHttpCookieAccess (
9901033 new JavaNetHttpCookieAccess () {
9911034 public List <HttpCookie > parse (String header ) {
992- return HttpCookie .parse (header , true );
1035+ return HttpCookie .parse (header , true , - 1L );
9931036 }
9941037
9951038 public String header (HttpCookie cookie ) {
0 commit comments