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