View Javadoc
1   /*
2    * Copyright 2012 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.codec.http;
17  
18  import static io.netty.handler.codec.http.CookieUtil.firstInvalidCookieNameOctet;
19  import static io.netty.handler.codec.http.CookieUtil.firstInvalidCookieValueOctet;
20  import static io.netty.handler.codec.http.CookieUtil.unwrapValue;
21  
22  import io.netty.handler.codec.DateFormatter;
23  import io.netty.handler.codec.http.cookie.CookieHeaderNames;
24  import io.netty.util.internal.logging.InternalLogger;
25  import io.netty.util.internal.logging.InternalLoggerFactory;
26  
27  import java.util.ArrayList;
28  import java.util.Collections;
29  import java.util.Date;
30  import java.util.List;
31  import java.util.Set;
32  import java.util.TreeSet;
33  
34  /**
35   * @deprecated Use {@link io.netty.handler.codec.http.cookie.ClientCookieDecoder}
36   * or {@link io.netty.handler.codec.http.cookie.ServerCookieDecoder} instead.
37   *
38   * Decodes an HTTP header value into {@link Cookie}s.  This decoder can decode
39   * the HTTP cookie version 0, 1, and 2.
40   *
41   * <pre>
42   * {@link HttpRequest} req = ...;
43   * String value = req.getHeader("Cookie");
44   * Set&lt;{@link Cookie}&gt; cookies = {@link CookieDecoder}.decode(value);
45   * </pre>
46   *
47   * @see io.netty.handler.codec.http.cookie.ClientCookieDecoder
48   * @see io.netty.handler.codec.http.cookie.ServerCookieDecoder
49   */
50  @Deprecated
51  public final class CookieDecoder {
52  
53      private final InternalLogger logger = InternalLoggerFactory.getInstance(getClass());
54  
55      private static final CookieDecoder STRICT = new CookieDecoder(true);
56  
57      private static final CookieDecoder LAX = new CookieDecoder(false);
58  
59      private static final String COMMENT = "Comment";
60  
61      private static final String COMMENTURL = "CommentURL";
62  
63      private static final String DISCARD = "Discard";
64  
65      private static final String PORT = "Port";
66  
67      private static final String VERSION = "Version";
68  
69      private final boolean strict;
70  
71      public static Set<Cookie> decode(String header) {
72          return decode(header, true);
73      }
74  
75      public static Set<Cookie> decode(String header, boolean strict) {
76          return (strict ? STRICT : LAX).doDecode(header);
77      }
78  
79      /**
80       * Decodes the specified HTTP header value into {@link Cookie}s.
81       *
82       * @return the decoded {@link Cookie}s
83       */
84      private Set<Cookie> doDecode(String header) {
85          List<String> names = new ArrayList<String>(8);
86          List<String> values = new ArrayList<String>(8);
87          extractKeyValuePairs(header, names, values);
88  
89          if (names.isEmpty()) {
90              return Collections.emptySet();
91          }
92  
93          int i;
94          int version = 0;
95  
96          // $Version is the only attribute that can appear before the actual
97          // cookie name-value pair.
98          if (names.get(0).equalsIgnoreCase(VERSION)) {
99              try {
100                 version = Integer.parseInt(values.get(0));
101             } catch (NumberFormatException e) {
102                 // Ignore.
103             }
104             i = 1;
105         } else {
106             i = 0;
107         }
108 
109         if (names.size() <= i) {
110             // There's a version attribute, but nothing more.
111             return Collections.emptySet();
112         }
113 
114         Set<Cookie> cookies = new TreeSet<Cookie>();
115         for (; i < names.size(); i ++) {
116             String name = names.get(i);
117             String value = values.get(i);
118             if (value == null) {
119                 value = "";
120             }
121 
122             Cookie c = initCookie(name, value);
123 
124             if (c == null) {
125                 break;
126             }
127 
128             boolean discard = false;
129             boolean secure = false;
130             boolean httpOnly = false;
131             String comment = null;
132             String commentURL = null;
133             String domain = null;
134             String path = null;
135             long maxAge = Long.MIN_VALUE;
136             List<Integer> ports = new ArrayList<Integer>(2);
137 
138             for (int j = i + 1; j < names.size(); j++, i++) {
139                 name = names.get(j);
140                 value = values.get(j);
141 
142                 if (DISCARD.equalsIgnoreCase(name)) {
143                     discard = true;
144                 } else if (CookieHeaderNames.SECURE.equalsIgnoreCase(name)) {
145                     secure = true;
146                 } else if (CookieHeaderNames.HTTPONLY.equalsIgnoreCase(name)) {
147                    httpOnly = true;
148                 } else if (COMMENT.equalsIgnoreCase(name)) {
149                     comment = value;
150                 } else if (COMMENTURL.equalsIgnoreCase(name)) {
151                     commentURL = value;
152                 } else if (CookieHeaderNames.DOMAIN.equalsIgnoreCase(name)) {
153                     domain = value;
154                 } else if (CookieHeaderNames.PATH.equalsIgnoreCase(name)) {
155                     path = value;
156                 } else if (CookieHeaderNames.EXPIRES.equalsIgnoreCase(name)) {
157                     Date date = DateFormatter.parseHttpDate(value);
158                     if (date != null) {
159                         long maxAgeMillis = date.getTime() - System.currentTimeMillis();
160                         maxAge = maxAgeMillis / 1000 + (maxAgeMillis % 1000 != 0? 1 : 0);
161                     }
162                 } else if (CookieHeaderNames.MAX_AGE.equalsIgnoreCase(name)) {
163                     maxAge = Integer.parseInt(value);
164                 } else if (VERSION.equalsIgnoreCase(name)) {
165                     version = Integer.parseInt(value);
166                 } else if (PORT.equalsIgnoreCase(name)) {
167                     String[] portList = value.split(",");
168                     for (String s1: portList) {
169                         try {
170                             ports.add(Integer.valueOf(s1));
171                         } catch (NumberFormatException e) {
172                             // Ignore.
173                         }
174                     }
175                 } else {
176                     break;
177                 }
178             }
179 
180             c.setVersion(version);
181             c.setMaxAge(maxAge);
182             c.setPath(path);
183             c.setDomain(domain);
184             c.setSecure(secure);
185             c.setHttpOnly(httpOnly);
186             if (version > 0) {
187                 c.setComment(comment);
188             }
189             if (version > 1) {
190                 c.setCommentUrl(commentURL);
191                 c.setPorts(ports);
192                 c.setDiscard(discard);
193             }
194 
195             cookies.add(c);
196         }
197 
198         return cookies;
199     }
200 
201     private static void extractKeyValuePairs(
202             final String header, final List<String> names, final List<String> values) {
203         final int headerLen  = header.length();
204         loop: for (int i = 0;;) {
205 
206             // Skip spaces and separators.
207             for (;;) {
208                 if (i == headerLen) {
209                     break loop;
210                 }
211                 switch (header.charAt(i)) {
212                 case '\t': case '\n': case 0x0b: case '\f': case '\r':
213                 case ' ':  case ',':  case ';':
214                     i ++;
215                     continue;
216                 }
217                 break;
218             }
219 
220             // Skip '$'.
221             for (;;) {
222                 if (i == headerLen) {
223                     break loop;
224                 }
225                 if (header.charAt(i) == '$') {
226                     i ++;
227                     continue;
228                 }
229                 break;
230             }
231 
232             String name;
233             String value;
234 
235             if (i == headerLen) {
236                 name = null;
237                 value = null;
238             } else {
239                 int newNameStart = i;
240                 keyValLoop: for (;;) {
241                     switch (header.charAt(i)) {
242                     case ';':
243                         // NAME; (no value till ';')
244                         name = header.substring(newNameStart, i);
245                         value = null;
246                         break keyValLoop;
247                     case '=':
248                         // NAME=VALUE
249                         name = header.substring(newNameStart, i);
250                         i ++;
251                         if (i == headerLen) {
252                             // NAME= (empty value, i.e. nothing after '=')
253                             value = "";
254                             break keyValLoop;
255                         }
256 
257                         int newValueStart = i;
258                         char c = header.charAt(i);
259                         if (c == '"' || c == '\'') {
260                             // NAME="VALUE" or NAME='VALUE'
261                             StringBuilder newValueBuf = new StringBuilder(header.length() - i);
262                             final char q = c;
263                             boolean hadBackslash = false;
264                             i ++;
265                             for (;;) {
266                                 if (i == headerLen) {
267                                     value = newValueBuf.toString();
268                                     break keyValLoop;
269                                 }
270                                 if (hadBackslash) {
271                                     hadBackslash = false;
272                                     c = header.charAt(i ++);
273                                     switch (c) {
274                                     case '\\': case '"': case '\'':
275                                         // Escape last backslash.
276                                         newValueBuf.setCharAt(newValueBuf.length() - 1, c);
277                                         break;
278                                     default:
279                                         // Do not escape last backslash.
280                                         newValueBuf.append(c);
281                                     }
282                                 } else {
283                                     c = header.charAt(i ++);
284                                     if (c == q) {
285                                         value = newValueBuf.toString();
286                                         break keyValLoop;
287                                     }
288                                     newValueBuf.append(c);
289                                     if (c == '\\') {
290                                         hadBackslash = true;
291                                     }
292                                 }
293                             }
294                         } else {
295                             // NAME=VALUE;
296                             int semiPos = header.indexOf(';', i);
297                             if (semiPos > 0) {
298                                 value = header.substring(newValueStart, semiPos);
299                                 i = semiPos;
300                             } else {
301                                 value = header.substring(newValueStart);
302                                 i = headerLen;
303                             }
304                         }
305                         break keyValLoop;
306                     default:
307                         i ++;
308                     }
309 
310                     if (i == headerLen) {
311                         // NAME (no value till the end of string)
312                         name = header.substring(newNameStart);
313                         value = null;
314                         break;
315                     }
316                 }
317             }
318 
319             names.add(name);
320             values.add(value);
321         }
322     }
323 
324     private CookieDecoder(boolean strict) {
325         this.strict = strict;
326     }
327 
328     private DefaultCookie initCookie(String name, String value) {
329         if (name == null || name.length() == 0) {
330             logger.debug("Skipping cookie with null name");
331             return null;
332         }
333 
334         if (value == null) {
335             logger.debug("Skipping cookie with null value");
336             return null;
337         }
338 
339         CharSequence unwrappedValue = unwrapValue(value);
340         if (unwrappedValue == null) {
341             logger.debug("Skipping cookie because starting quotes are not properly balanced in '{}'",
342                     unwrappedValue);
343             return null;
344         }
345 
346         int invalidOctetPos;
347         if (strict && (invalidOctetPos = firstInvalidCookieNameOctet(name)) >= 0) {
348             if (logger.isDebugEnabled()) {
349                 logger.debug("Skipping cookie because name '{}' contains invalid char '{}'",
350                         name, name.charAt(invalidOctetPos));
351             }
352             return null;
353         }
354 
355         final boolean wrap = unwrappedValue.length() != value.length();
356 
357         if (strict && (invalidOctetPos = firstInvalidCookieValueOctet(unwrappedValue)) >= 0) {
358             if (logger.isDebugEnabled()) {
359                 logger.debug("Skipping cookie because value '{}' contains invalid char '{}'",
360                         unwrappedValue, unwrappedValue.charAt(invalidOctetPos));
361             }
362             return null;
363         }
364 
365         DefaultCookie cookie = new DefaultCookie(name, unwrappedValue.toString());
366         cookie.setWrap(wrap);
367         return cookie;
368     }
369 }