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