View Javadoc

1   /*
2    * Copyright 2009 Red Hat, Inc.
3    *
4    * Red Hat licenses this file to you under the Apache License, version 2.0
5    * (the "License"); you may not use this file except in compliance with the
6    * 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.http2;
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  import java.util.regex.Matcher;
25  import java.util.regex.Pattern;
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   * @author <a href="http://www.jboss.org/netty/">The Netty Project</a>
38   * @author Andy Taylor (andy.taylor@jboss.org)
39   * @author <a href="http://gleamynode.net/">Trustin Lee</a>
40   * @version $Rev: 1107 $, $Date: 2012-04-15 19:00:57 +0200 (dim., 15 avr. 2012) $
41   * @see CookieEncoder
42   *
43   * @apiviz.stereotype utility
44   * @apiviz.has        org.jboss.netty.handler.codec.http2.Cookie oneway - - decodes
45   */
46  public class CookieDecoder {
47  
48      private final static Pattern PATTERN =
49          Pattern.compile("(?:\\s|[;,])*\\$*([^;=]+)(?:=(?:[\"']((?:\\\\.|[^\"])*)[\"']|([^;,]*)))?(\\s*(?:[;,]+\\s*|$))");
50  
51      private final static String COMMA = ",";
52  
53      private final boolean lenient;
54      
55      /**
56       * Creates a new decoder.
57       */
58      public CookieDecoder() {
59          this(false);
60      }
61      
62      /**
63      * Creates a new decoder.
64      *
65      * @param lenient ignores cookies with the name 'HTTPOnly' instead of throwing an exception
66      */
67      public CookieDecoder(boolean lenient) {
68          this.lenient = lenient;
69      }
70  
71  
72      /**
73       * Decodes the specified HTTP header value into {@link Cookie}s.
74       *
75       * @return the decoded {@link Cookie}s
76       */
77      public Set<Cookie> decode(String header) {
78          List<String> names = new ArrayList<String>(8);
79          List<String> values = new ArrayList<String>(8);
80          extractKeyValuePairs(header, names, values);
81  
82          if (names.isEmpty()) {
83              return Collections.emptySet();
84          }
85  
86          int i;
87          int version = 0;
88  
89          // $Version is the only attribute that can appear before the actual
90          // cookie name-value pair.
91          if (names.get(0).equalsIgnoreCase(CookieHeaderNames.VERSION)) {
92              try {
93                  version = Integer.parseInt(values.get(0));
94              } catch (NumberFormatException e) {
95                  // Ignore.
96              }
97              i = 1;
98          } else {
99              i = 0;
100         }
101 
102         if (names.size() <= i) {
103             // There's a version attribute, but nothing more.
104             return Collections.emptySet();
105         }
106 
107         Set<Cookie> cookies = new TreeSet<Cookie>();
108         for (; i < names.size(); i ++) {
109             String name = names.get(i);
110             // Not all user agents understand the HttpOnly attribute
111             if (lenient && CookieHeaderNames.HTTPONLY.equalsIgnoreCase(name)) {
112                 continue;
113             }
114             String value = values.get(i);
115             if (value == null) {
116                 value = "";
117             }
118 
119             Cookie c = new DefaultCookie(name, value);
120             cookies.add(c);
121 
122             boolean discard = false;
123             boolean secure = false;
124             boolean httpOnly = false;
125             String comment = null;
126             String commentURL = null;
127             String domain = null;
128             String path = null;
129             int maxAge = -1;
130             List<Integer> ports = new ArrayList<Integer>(2);
131 
132             for (int j = i + 1; j < names.size(); j++, i++) {
133                 name = names.get(j);
134                 value = values.get(j);
135 
136                 if (CookieHeaderNames.DISCARD.equalsIgnoreCase(name)) {
137                     discard = true;
138                 } else if (CookieHeaderNames.SECURE.equalsIgnoreCase(name)) {
139                     secure = true;
140                 } else if (CookieHeaderNames.HTTPONLY.equalsIgnoreCase(name)) {
141                    httpOnly = true;
142                 } else if (CookieHeaderNames.COMMENT.equalsIgnoreCase(name)) {
143                     comment = value;
144                 } else if (CookieHeaderNames.COMMENTURL.equalsIgnoreCase(name)) {
145                     commentURL = value;
146                 } else if (CookieHeaderNames.DOMAIN.equalsIgnoreCase(name)) {
147                     domain = value;
148                 } else if (CookieHeaderNames.PATH.equalsIgnoreCase(name)) {
149                     path = value;
150                 } else if (CookieHeaderNames.EXPIRES.equalsIgnoreCase(name)) {
151                     try {
152                         long maxAgeMillis =
153                             new CookieDateFormat().parse(value).getTime() -
154                             System.currentTimeMillis();
155                         if (maxAgeMillis <= 0) {
156                             maxAge = 0;
157                         } else {
158                             maxAge = (int) (maxAgeMillis / 1000) +
159                                      (maxAgeMillis % 1000 != 0? 1 : 0);
160                         }
161                     } catch (ParseException e) {
162                         // Ignore.
163                     }
164                 } else if (CookieHeaderNames.MAX_AGE.equalsIgnoreCase(name)) {
165                     maxAge = Integer.parseInt(value);
166                 } else if (CookieHeaderNames.VERSION.equalsIgnoreCase(name)) {
167                     version = Integer.parseInt(value);
168                 } else if (CookieHeaderNames.PORT.equalsIgnoreCase(name)) {
169                     String[] portList = value.split(COMMA);
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 
198         return cookies;
199     }
200 
201     private void extractKeyValuePairs(
202             String header, List<String> names, List<String> values) {
203         Matcher m = PATTERN.matcher(header);
204         int pos = 0;
205         String name = null;
206         String value = null;
207         String separator = null;
208         while (m.find(pos)) {
209             pos = m.end();
210 
211             // Extract name and value pair from the match.
212             String newName = m.group(1);
213             String newValue = m.group(3);
214             if (newValue == null) {
215                 newValue = decodeValue(m.group(2));
216             }
217             String newSeparator = m.group(4);
218 
219             if (name == null) {
220                 name = newName;
221                 value = newValue == null? "" : newValue;
222                 separator = newSeparator;
223                 continue;
224             }
225 
226             if (newValue == null &&
227                 !CookieHeaderNames.DISCARD.equalsIgnoreCase(newName) &&
228                 !CookieHeaderNames.SECURE.equalsIgnoreCase(newName) &&
229                 !CookieHeaderNames.HTTPONLY.equalsIgnoreCase(newName)) {
230                 value = value + separator + newName;
231                 separator = newSeparator;
232                 continue;
233             }
234 
235             names.add(name);
236             values.add(value);
237 
238             name = newName;
239             value = newValue;
240             separator = newSeparator;
241         }
242 
243         // The last entry
244         if (name != null) {
245             names.add(name);
246             values.add(value);
247         }
248     }
249 
250     private String decodeValue(String value) {
251         if (value == null) {
252             return value;
253         }
254         return value.replace("\\\"", "\"").replace("\\\\", "\\");
255     }
256 }