View Javadoc
1   /*
2    * Copyright 2014 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 io.netty.handler.codec.http;
17  
18  import java.text.ParsePosition;
19  import java.util.Date;
20  
21  import static io.netty.handler.codec.http.CookieEncoderUtil.*;
22  
23  /**
24   * A <a href="http://tools.ietf.org/html/rfc6265">RFC6265</a> compliant cookie decoder to be used client side.
25   *
26   * It will store the raw value in {@link Cookie#setRawValue(String)} so it can be
27   * eventually sent back to the Origin server as is.
28   *
29   * @see ClientCookieEncoder
30   */
31  public final class ClientCookieDecoder {
32  
33      /**
34       * Decodes the specified Set-Cookie HTTP header value into a {@link Cookie}.
35       *
36       * @return the decoded {@link Cookie}
37       */
38      public static Cookie decode(String header) {
39  
40          if (header == null) {
41              throw new NullPointerException("header");
42          }
43  
44          final int headerLen = header.length();
45  
46          if (headerLen == 0) {
47              return null;
48          }
49  
50          CookieBuilder cookieBuilder = null;
51  
52          loop: for (int i = 0;;) {
53  
54              // Skip spaces and separators.
55              for (;;) {
56                  if (i == headerLen) {
57                      break loop;
58                  }
59                  char c = header.charAt(i);
60                  if (c == ',') {
61                      // Having multiple cookies in a single Set-Cookie header is
62                      // deprecated, modern browsers only parse the first one
63                      break loop;
64  
65                  } else if (c == '\t' || c == '\n' || c == 0x0b || c == '\f'
66                          || c == '\r' || c == ' ' || c == ';') {
67                      i++;
68                      continue;
69                  }
70                  break;
71              }
72  
73              int newNameStart = i;
74              int newNameEnd = i;
75              String value, rawValue;
76  
77              if (i == headerLen) {
78                  value = rawValue = null;
79              } else {
80                  keyValLoop: for (;;) {
81  
82                      char curChar = header.charAt(i);
83                      if (curChar == ';') {
84                          // NAME; (no value till ';')
85                          newNameEnd = i;
86                          value = rawValue = null;
87                          break keyValLoop;
88                      } else if (curChar == '=') {
89                          // NAME=VALUE
90                          newNameEnd = i;
91                          i++;
92                          if (i == headerLen) {
93                              // NAME= (empty value, i.e. nothing after '=')
94                              value = rawValue = "";
95                              break keyValLoop;
96                          }
97  
98                          int newValueStart = i;
99                          char c = header.charAt(i);
100                         if (c == '"') {
101                             // NAME="VALUE"
102                             StringBuilder newValueBuf = stringBuilder();
103 
104                             int rawValueStart = i;
105                             int rawValueEnd = i;
106 
107                             final char q = c;
108                             boolean hadBackslash = false;
109                             i++;
110                             for (;;) {
111                                 if (i == headerLen) {
112                                     value = newValueBuf.toString();
113                                     // only need to compute raw value for cookie
114                                     // value which is in first position
115                                     rawValue = header.substring(rawValueStart, rawValueEnd);
116                                     break keyValLoop;
117                                 }
118                                 if (hadBackslash) {
119                                     hadBackslash = false;
120                                     c = header.charAt(i++);
121                                     rawValueEnd = i;
122                                     if (c == '\\' || c == '"') {
123                                         newValueBuf.setCharAt(newValueBuf.length() - 1, c);
124                                     } else {
125                                         // Do not escape last backslash.
126                                         newValueBuf.append(c);
127                                     }
128                                 } else {
129                                     c = header.charAt(i++);
130                                     rawValueEnd = i;
131                                     if (c == q) {
132                                         value = newValueBuf.toString();
133                                         // only need to compute raw value for
134                                         // cookie value which is in first
135                                         // position
136                                         rawValue = header.substring(rawValueStart, rawValueEnd);
137                                         break keyValLoop;
138                                     }
139                                     newValueBuf.append(c);
140                                     if (c == '\\') {
141                                         hadBackslash = true;
142                                     }
143                                 }
144                             }
145                         } else {
146                             // NAME=VALUE;
147                             int semiPos = header.indexOf(';', i);
148                             if (semiPos > 0) {
149                                 value = rawValue = header.substring(newValueStart, semiPos);
150                                 i = semiPos;
151                             } else {
152                                 value = rawValue = header.substring(newValueStart);
153                                 i = headerLen;
154                             }
155                         }
156                         break keyValLoop;
157                     } else {
158                         i++;
159                     }
160 
161                     if (i == headerLen) {
162                         // NAME (no value till the end of string)
163                         newNameEnd = i;
164                         value = rawValue = null;
165                         break;
166                     }
167                 }
168             }
169 
170             if (cookieBuilder == null) {
171                 cookieBuilder = new CookieBuilder(header, newNameStart, newNameEnd, value, rawValue);
172             } else {
173                 cookieBuilder.appendAttribute(header, newNameStart, newNameEnd, value);
174             }
175         }
176         return cookieBuilder.cookie();
177     }
178 
179     private static class CookieBuilder {
180 
181         private final String name;
182         private final String value;
183         private final String rawValue;
184         private String domain;
185         private String path;
186         private long maxAge = Long.MIN_VALUE;
187         private String expires;
188         private boolean secure;
189         private boolean httpOnly;
190 
191         public CookieBuilder(String header, int keyStart, int keyEnd,
192                 String value, String rawValue) {
193             name = header.substring(keyStart, keyEnd);
194             this.value = value;
195             this.rawValue = rawValue;
196         }
197 
198         private long mergeMaxAgeAndExpire(long maxAge, String expires) {
199             // max age has precedence over expires
200             if (maxAge != Long.MIN_VALUE) {
201                 return maxAge;
202             } else if (expires != null) {
203                 Date expiresDate = HttpHeaderDateFormat.get().parse(expires, new ParsePosition(0));
204                 if (expiresDate != null) {
205                     long maxAgeMillis = expiresDate.getTime() - System.currentTimeMillis();
206                     return maxAgeMillis / 1000 + (maxAgeMillis % 1000 != 0 ? 1 : 0);
207                 }
208             }
209             return Long.MIN_VALUE;
210         }
211 
212         public Cookie cookie() {
213             if (name == null) {
214                 return null;
215             }
216 
217             DefaultCookie cookie = new DefaultCookie(name, value);
218             cookie.setValue(value);
219             cookie.setRawValue(rawValue);
220             cookie.setDomain(domain);
221             cookie.setPath(path);
222             cookie.setMaxAge(mergeMaxAgeAndExpire(maxAge, expires));
223             cookie.setSecure(secure);
224             cookie.setHttpOnly(httpOnly);
225             return cookie;
226         }
227 
228         /**
229          * Parse and store a key-value pair. First one is considered to be the
230          * cookie name/value. Unknown attribute names are silently discarded.
231          *
232          * @param header
233          *            the HTTP header
234          * @param keyStart
235          *            where the key starts in the header
236          * @param keyEnd
237          *            where the key ends in the header
238          * @param value
239          *            the decoded value
240          */
241         public void appendAttribute(String header, int keyStart, int keyEnd,
242                 String value) {
243             setCookieAttribute(header, keyStart, keyEnd, value);
244         }
245 
246         private void setCookieAttribute(String header, int keyStart,
247                 int keyEnd, String value) {
248 
249             int length = keyEnd - keyStart;
250 
251             if (length == 4) {
252                 parse4(header, keyStart, value);
253             } else if (length == 6) {
254                 parse6(header, keyStart, value);
255             } else if (length == 7) {
256                 parse7(header, keyStart, value);
257             } else if (length == 8) {
258                 parse8(header, keyStart, value);
259             }
260         }
261 
262         private void parse4(String header, int nameStart, String value) {
263             if (header.regionMatches(true, nameStart, "Path", 0, 4)) {
264                 path = value;
265             }
266         }
267 
268         private void parse6(String header, int nameStart, String value) {
269             if (header.regionMatches(true, nameStart, "Domain", 0, 5)) {
270                 domain = value.isEmpty() ? null : value;
271             } else if (header.regionMatches(true, nameStart, "Secure", 0, 5)) {
272                 secure = true;
273             }
274         }
275 
276         private void setExpire(String value) {
277             expires = value;
278         }
279 
280         private void setMaxAge(String value) {
281             try {
282                 maxAge = Math.max(Long.valueOf(value), 0L);
283             } catch (NumberFormatException e1) {
284                 // ignore failure to parse -> treat as session cookie
285             }
286         }
287 
288         private void parse7(String header, int nameStart, String value) {
289             if (header.regionMatches(true, nameStart, "Expires", 0, 7)) {
290                 setExpire(value);
291             } else if (header.regionMatches(true, nameStart, "Max-Age", 0, 7)) {
292                 setMaxAge(value);
293             }
294         }
295 
296         private void parse8(String header, int nameStart, String value) {
297 
298             if (header.regionMatches(true, nameStart, "HttpOnly", 0, 8)) {
299                 httpOnly = true;
300             }
301         }
302     }
303 
304     private ClientCookieDecoder() {
305         // unused
306     }
307 }