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 java.text.ParseException;
19  import java.util.ArrayList;
20  import java.util.Collections;
21  import java.util.List;
22  import java.util.Set;
23  import java.util.TreeSet;
24  
25  /**
26   * Decodes an HTTP header value into {@link Cookie}s.  This decoder can decode
27   * the HTTP cookie version 0, 1, and 2.
28   *
29   * <pre>
30   * {@link HttpRequest} req = ...;
31   * String value = req.getHeader("Cookie");
32   * Set&lt;{@link Cookie}&gt; cookies = new {@link CookieDecoder}().decode(value);
33   * </pre>
34   *
35   * @see CookieEncoder
36   *
37   * @apiviz.stereotype utility
38   * @apiviz.has        org.jboss.netty.handler.codec.http.Cookie oneway - - decodes
39   */
40  public class CookieDecoder {
41  
42      private static final String COMMA = ",";
43  
44      /**
45       * Creates a new decoder.
46       */
47      public CookieDecoder() {
48          super();
49      }
50  
51      /**
52       * @deprecated Use {@link #CookieDecoder()} instead.
53       */
54      @Deprecated
55      public CookieDecoder(@SuppressWarnings("unused") boolean lenient) {
56          super();
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                             new CookieDateFormat().parse(value).getTime() -
136                             System.currentTimeMillis();
137                         if (maxAgeMillis <= 0) {
138                             maxAge = 0;
139                         } else {
140                             maxAge = (int) (maxAgeMillis / 1000) +
141                                      (maxAgeMillis % 1000 != 0? 1 : 0);
142                         }
143                     } catch (ParseException e) {
144                         // Ignore.
145                     }
146                 } else if (CookieHeaderNames.MAX_AGE.equalsIgnoreCase(name)) {
147                     maxAge = Integer.parseInt(value);
148                 } else if (CookieHeaderNames.VERSION.equalsIgnoreCase(name)) {
149                     version = Integer.parseInt(value);
150                 } else if (CookieHeaderNames.PORT.equalsIgnoreCase(name)) {
151                     String[] portList = value.split(COMMA);
152                     for (String s1: portList) {
153                         try {
154                             ports.add(Integer.valueOf(s1));
155                         } catch (NumberFormatException e) {
156                             // Ignore.
157                         }
158                     }
159                 } else {
160                     break;
161                 }
162             }
163 
164             c.setVersion(version);
165             c.setMaxAge(maxAge);
166             c.setPath(path);
167             c.setDomain(domain);
168             c.setSecure(secure);
169             c.setHttpOnly(httpOnly);
170             if (version > 0) {
171                 c.setComment(comment);
172             }
173             if (version > 1) {
174                 c.setCommentUrl(commentURL);
175                 c.setPorts(ports);
176                 c.setDiscard(discard);
177             }
178 
179             cookies.add(c);
180         }
181 
182         return cookies;
183     }
184 
185 
186     private static void extractKeyValuePairs(
187             final String header, final List<String> names, final List<String> values) {
188 
189         final int headerLen  = header.length();
190         loop: for (int i = 0;;) {
191 
192             // Skip spaces and separators.
193             for (;;) {
194                 if (i == headerLen) {
195                     break loop;
196                 }
197                 switch (header.charAt(i)) {
198                 case '\t': case '\n': case 0x0b: case '\f': case '\r':
199                 case ' ':  case ',':  case ';':
200                     i ++;
201                     continue;
202                 }
203                 break;
204             }
205 
206             // Skip '$'.
207             for (;;) {
208                 if (i == headerLen) {
209                     break loop;
210                 }
211                 if (header.charAt(i) == '$') {
212                     i ++;
213                     continue;
214                 }
215                 break;
216             }
217 
218             String name;
219             String value;
220 
221             if (i == headerLen) {
222                 name = null;
223                 value = null;
224             } else {
225                 int newNameStart = i;
226                 keyValLoop: for (;;) {
227                     switch (header.charAt(i)) {
228                     case ';':
229                         // NAME; (no value till ';')
230                         name = header.substring(newNameStart, i);
231                         value = null;
232                         break keyValLoop;
233                     case '=':
234                         // NAME=VALUE
235                         name = header.substring(newNameStart, i);
236                         i ++;
237                         if (i == headerLen) {
238                             // NAME= (empty value, i.e. nothing after '=')
239                             value = "";
240                             break keyValLoop;
241                         }
242 
243                         int newValueStart = i;
244                         char c = header.charAt(i);
245                         if (c == '"' || c == '\'') {
246                             // NAME="VALUE" or NAME='VALUE'
247                             StringBuilder newValueBuf = new StringBuilder(header.length() - i);
248                             final char q = c;
249                             boolean hadBackslash = false;
250                             i ++;
251                             for (;;) {
252                                 if (i == headerLen) {
253                                     value = newValueBuf.toString();
254                                     break keyValLoop;
255                                 }
256                                 if (hadBackslash) {
257                                     hadBackslash = false;
258                                     c = header.charAt(i ++);
259                                     switch (c) {
260                                     case '\\': case '"': case '\'':
261                                         // Escape last backslash.
262                                         newValueBuf.setCharAt(newValueBuf.length() - 1, c);
263                                         break;
264                                     default:
265                                         // Do not escape last backslash.
266                                         newValueBuf.append(c);
267                                     }
268                                 } else {
269                                     c = header.charAt(i ++);
270                                     if (c == q) {
271                                         value = newValueBuf.toString();
272                                         break keyValLoop;
273                                     }
274                                     newValueBuf.append(c);
275                                     if (c == '\\') {
276                                         hadBackslash = true;
277                                     }
278                                 }
279                             }
280                         } else {
281                             // NAME=VALUE;
282                             int semiPos = header.indexOf(';', i);
283                             if (semiPos > 0) {
284                                 value = header.substring(newValueStart, semiPos);
285                                 i = semiPos;
286                             } else {
287                                 value = header.substring(newValueStart);
288                                 i = headerLen;
289                             }
290                         }
291                         break keyValLoop;
292                     default:
293                         i ++;
294                     }
295 
296                     if (i == headerLen) {
297                         // NAME (no value till the end of string)
298                         name = header.substring(newNameStart);
299                         value = null;
300                         break;
301                     }
302                 }
303             }
304 
305             names.add(name);
306             values.add(value);
307         }
308     }
309 }