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