View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  package org.apache.hc.client5.http.impl.cookie;
28  
29  import java.time.Instant;
30  import java.time.Month;
31  import java.time.ZoneId;
32  import java.time.ZonedDateTime;
33  import java.util.BitSet;
34  import java.util.Locale;
35  import java.util.Map;
36  import java.util.concurrent.ConcurrentHashMap;
37  import java.util.regex.Matcher;
38  import java.util.regex.Pattern;
39  
40  import org.apache.hc.client5.http.cookie.CommonCookieAttributeHandler;
41  import org.apache.hc.client5.http.cookie.Cookie;
42  import org.apache.hc.client5.http.cookie.MalformedCookieException;
43  import org.apache.hc.client5.http.cookie.SetCookie;
44  import org.apache.hc.core5.annotation.Contract;
45  import org.apache.hc.core5.annotation.ThreadingBehavior;
46  import org.apache.hc.core5.util.Args;
47  import org.apache.hc.core5.util.TextUtils;
48  import org.apache.hc.core5.util.Tokenizer;
49  
50  /**
51   * Cookie {@code expires} attribute handler conformant to the more relaxed interpretation
52   * of HTTP state management.
53   *
54   * @since 4.4
55   */
56  @Contract(threading = ThreadingBehavior.STATELESS)
57  public class LaxExpiresHandler extends AbstractCookieAttributeHandler implements CommonCookieAttributeHandler {
58  
59      /**
60       * Singleton instance.
61       *
62       * @since 5.2
63       */
64      public static final LaxExpiresHandler INSTANCE = new LaxExpiresHandler();
65  
66      private static final BitSet DELIMS;
67      static {
68          final BitSet bitSet = new BitSet();
69          bitSet.set(0x9);
70          for (int b = 0x20; b <= 0x2f; b++) {
71              bitSet.set(b);
72          }
73          for (int b = 0x3b; b <= 0x40; b++) {
74              bitSet.set(b);
75          }
76          for (int b = 0x5b; b <= 0x60; b++) {
77              bitSet.set(b);
78          }
79          for (int b = 0x7b; b <= 0x7e; b++) {
80              bitSet.set(b);
81          }
82          DELIMS = bitSet;
83      }
84      private static final Map<String, Month> MONTHS;
85      static {
86          final ConcurrentHashMap<String, Month> map = new ConcurrentHashMap<>(12);
87          map.put("jan", Month.JANUARY);
88          map.put("feb", Month.FEBRUARY);
89          map.put("mar", Month.MARCH);
90          map.put("apr", Month.APRIL);
91          map.put("may", Month.MAY);
92          map.put("jun", Month.JUNE);
93          map.put("jul", Month.JULY);
94          map.put("aug", Month.AUGUST);
95          map.put("sep", Month.SEPTEMBER);
96          map.put("oct", Month.OCTOBER);
97          map.put("nov", Month.NOVEMBER);
98          map.put("dec", Month.DECEMBER);
99          MONTHS = map;
100     }
101 
102     private final static Pattern TIME_PATTERN = Pattern.compile(
103             "^([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})([^0-9].*)?$");
104     private final static Pattern DAY_OF_MONTH_PATTERN = Pattern.compile(
105             "^([0-9]{1,2})([^0-9].*)?$");
106     private final static Pattern MONTH_PATTERN = Pattern.compile(
107             "^(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)(.*)?$", Pattern.CASE_INSENSITIVE);
108     private final static Pattern YEAR_PATTERN = Pattern.compile(
109             "^([0-9]{2,4})([^0-9].*)?$");
110 
111     public LaxExpiresHandler() {
112         super();
113     }
114 
115     @Override
116     public void parse(final SetCookie cookie, final String value) throws MalformedCookieException {
117         Args.notNull(cookie, "Cookie");
118         if (TextUtils.isBlank(value)) {
119             return;
120         }
121         final Tokenizer.Cursor cursor = new Tokenizer.Cursor(0, value.length());
122         final StringBuilder content = new StringBuilder();
123 
124         int second = 0, minute = 0, hour = 0, day = 0, year = 0;
125         Month month = Month.JANUARY;
126         boolean foundTime = false, foundDayOfMonth = false, foundMonth = false, foundYear = false;
127         try {
128             while (!cursor.atEnd()) {
129                 skipDelims(value, cursor);
130                 content.setLength(0);
131                 copyContent(value, cursor, content);
132 
133                 if (content.length() == 0) {
134                     break;
135                 }
136                 if (!foundTime) {
137                     final Matcher matcher = TIME_PATTERN.matcher(content);
138                     if (matcher.matches()) {
139                         foundTime = true;
140                         hour = Integer.parseInt(matcher.group(1));
141                         minute = Integer.parseInt(matcher.group(2));
142                         second =Integer.parseInt(matcher.group(3));
143                         continue;
144                     }
145                 }
146                 if (!foundDayOfMonth) {
147                     final Matcher matcher = DAY_OF_MONTH_PATTERN.matcher(content);
148                     if (matcher.matches()) {
149                         foundDayOfMonth = true;
150                         day = Integer.parseInt(matcher.group(1));
151                         continue;
152                     }
153                 }
154                 if (!foundMonth) {
155                     final Matcher matcher = MONTH_PATTERN.matcher(content);
156                     if (matcher.matches()) {
157                         foundMonth = true;
158                         month = MONTHS.get(matcher.group(1).toLowerCase(Locale.ROOT));
159                         continue;
160                     }
161                 }
162                 if (!foundYear) {
163                     final Matcher matcher = YEAR_PATTERN.matcher(content);
164                     if (matcher.matches()) {
165                         foundYear = true;
166                         year = Integer.parseInt(matcher.group(1));
167                         continue;
168                     }
169                 }
170             }
171         } catch (final NumberFormatException ignore) {
172             throw new MalformedCookieException("Invalid 'expires' attribute: " + value);
173         }
174         if (!foundTime || !foundDayOfMonth || !foundMonth || !foundYear) {
175             throw new MalformedCookieException("Invalid 'expires' attribute: " + value);
176         }
177         if (year >= 70 && year <= 99) {
178             year = 1900 + year;
179         }
180         if (year >= 0 && year <= 69) {
181             year = 2000 + year;
182         }
183         if (day < 1 || day > 31 || year < 1601 || hour > 23 || minute > 59 || second > 59) {
184             throw new MalformedCookieException("Invalid 'expires' attribute: " + value);
185         }
186 
187         final Instant expiryDate = ZonedDateTime.of(year, month.getValue(), day, hour, minute, second, 0,
188                 ZoneId.of("UTC")).toInstant();
189         cookie.setExpiryDate(expiryDate);
190     }
191 
192     private void skipDelims(final CharSequence buf, final Tokenizer.Cursor cursor) {
193         int pos = cursor.getPos();
194         final int indexFrom = cursor.getPos();
195         final int indexTo = cursor.getUpperBound();
196         for (int i = indexFrom; i < indexTo; i++) {
197             final char current = buf.charAt(i);
198             if (DELIMS.get(current)) {
199                 pos++;
200             } else {
201                 break;
202             }
203         }
204         cursor.updatePos(pos);
205     }
206 
207     private void copyContent(final CharSequence buf, final Tokenizer.Cursor cursor, final StringBuilder dst) {
208         int pos = cursor.getPos();
209         final int indexFrom = cursor.getPos();
210         final int indexTo = cursor.getUpperBound();
211         for (int i = indexFrom; i < indexTo; i++) {
212             final char current = buf.charAt(i);
213             if (DELIMS.get(current)) {
214                 break;
215             }
216             pos++;
217             dst.append(current);
218         }
219         cursor.updatePos(pos);
220     }
221 
222     @Override
223     public String getAttributeName() {
224         return Cookie.EXPIRES_ATTR;
225     }
226 
227 }