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                             new CookieDateFormat().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 
184     private static void extractKeyValuePairs(
185             final String header, final List<String> names, final List<String> values) {
186 
187         final int headerLen  = header.length();
188         loop: for (int i = 0;;) {
189 
190             // Skip spaces and separators.
191             for (;;) {
192                 if (i == headerLen) {
193                     break loop;
194                 }
195                 switch (header.charAt(i)) {
196                 case '\t': case '\n': case 0x0b: case '\f': case '\r':
197                 case ' ':  case ',':  case ';':
198                     i ++;
199                     continue;
200                 }
201                 break;
202             }
203 
204             // Skip '$'.
205             for (;;) {
206                 if (i == headerLen) {
207                     break loop;
208                 }
209                 if (header.charAt(i) == '$') {
210                     i ++;
211                     continue;
212                 }
213                 break;
214             }
215 
216             String name;
217             String value;
218 
219             if (i == headerLen) {
220                 name = null;
221                 value = null;
222             } else {
223                 int newNameStart = i;
224                 keyValLoop: for (;;) {
225                     switch (header.charAt(i)) {
226                     case ';':
227                         // NAME; (no value till ';')
228                         name = header.substring(newNameStart, i);
229                         value = null;
230                         break keyValLoop;
231                     case '=':
232                         // NAME=VALUE
233                         name = header.substring(newNameStart, i);
234                         i ++;
235                         if (i == headerLen) {
236                             // NAME= (empty value, i.e. nothing after '=')
237                             value = "";
238                             break keyValLoop;
239                         }
240 
241                         int newValueStart = i;
242                         char c = header.charAt(i);
243                         if (c == '"' || c == '\'') {
244                             // NAME="VALUE" or NAME='VALUE'
245                             StringBuilder newValueBuf = new StringBuilder(header.length() - i);
246                             final char q = c;
247                             boolean hadBackslash = false;
248                             i ++;
249                             for (;;) {
250                                 if (i == headerLen) {
251                                     value = newValueBuf.toString();
252                                     break keyValLoop;
253                                 }
254                                 if (hadBackslash) {
255                                     hadBackslash = false;
256                                     c = header.charAt(i ++);
257                                     switch (c) {
258                                     case '\\': case '"': case '\'':
259                                         // Escape last backslash.
260                                         newValueBuf.setCharAt(newValueBuf.length() - 1, c);
261                                         break;
262                                     default:
263                                         // Do not escape last backslash.
264                                         newValueBuf.append(c);
265                                     }
266                                 } else {
267                                     c = header.charAt(i ++);
268                                     if (c == q) {
269                                         value = newValueBuf.toString();
270                                         break keyValLoop;
271                                     }
272                                     newValueBuf.append(c);
273                                     if (c == '\\') {
274                                         hadBackslash = true;
275                                     }
276                                 }
277                             }
278                         } else {
279                             // NAME=VALUE;
280                             int semiPos = header.indexOf(';', i);
281                             if (semiPos > 0) {
282                                 value = header.substring(newValueStart, semiPos);
283                                 i = semiPos;
284                             } else {
285                                 value = header.substring(newValueStart);
286                                 i = headerLen;
287                             }
288                         }
289                         break keyValLoop;
290                     default:
291                         i ++;
292                     }
293 
294                     if (i == headerLen) {
295                         // NAME (no value till the end of string)
296                         name = header.substring(newNameStart);
297                         value = null;
298                         break;
299                     }
300                 }
301             }
302 
303             names.add(name);
304             values.add(value);
305         }
306     }
307 }