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<{@link Cookie}> 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 }