View Javadoc
1   /*
2    * Copyright 2015 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.cookie;
17  
18  import org.jboss.netty.handler.codec.http.HttpHeaderDateFormat;
19  
20  import java.text.ParsePosition;
21  import java.util.Date;
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 way the raw value was wrapped in {@link Cookie#setWrap(boolean)} so it can be
27   * eventually sent back to the Origin server as is.
28   *
29   * @see ClientCookieEncoder
30   */
31  public final class ClientCookieDecoder extends CookieDecoder {
32  
33      /**
34       * Strict encoder that validates that name and value chars are in the valid scope
35       * defined in RFC6265
36       */
37      public static final ClientCookieDecoder STRICT = new ClientCookieDecoder(true);
38  
39      /**
40       * Lax instance that doesn't validate name and value
41       */
42      public static final ClientCookieDecoder LAX = new ClientCookieDecoder(false);
43  
44      private ClientCookieDecoder(boolean strict) {
45          super(strict);
46      }
47  
48      /**
49       * Decodes the specified Set-Cookie HTTP header value into a {@link Cookie}.
50       *
51       * @return the decoded {@link Cookie}
52       */
53      public Cookie decode(String header) {
54          if (header == null) {
55              throw new NullPointerException("header");
56          }
57          final int headerLen = header.length();
58  
59          if (headerLen == 0) {
60              return null;
61          }
62  
63          CookieBuilder cookieBuilder = null;
64  
65          loop: for (int i = 0;;) {
66  
67              // Skip spaces and separators.
68              for (;;) {
69                  if (i == headerLen) {
70                      break loop;
71                  }
72                  char c = header.charAt(i);
73                  if (c == ',') {
74                      // Having multiple cookies in a single Set-Cookie header is
75                      // deprecated, modern browsers only parse the first one
76                      break loop;
77  
78                  } else if (c == '\t' || c == '\n' || c == 0x0b || c == '\f'
79                          || c == '\r' || c == ' ' || c == ';') {
80                      i++;
81                      continue;
82                  }
83                  break;
84              }
85  
86              int nameBegin = i;
87              int nameEnd = i;
88              int valueBegin = -1;
89              int valueEnd = -1;
90  
91              if (i != headerLen) {
92                  keyValLoop: for (;;) {
93  
94                      char curChar = header.charAt(i);
95                      if (curChar == ';') {
96                          // NAME; (no value till ';')
97                          nameEnd = i;
98                          valueBegin = valueEnd = -1;
99                          break keyValLoop;
100 
101                     } else if (curChar == '=') {
102                         // NAME=VALUE
103                         nameEnd = i;
104                         i++;
105                         if (i == headerLen) {
106                             // NAME= (empty value, i.e. nothing after '=')
107                             valueBegin = valueEnd = 0;
108                             break keyValLoop;
109                         }
110 
111                         valueBegin = i;
112                         // NAME=VALUE;
113                         int semiPos = header.indexOf(';', i);
114                         valueEnd = i = semiPos > 0 ? semiPos : headerLen;
115                         break keyValLoop;
116                     } else {
117                         i++;
118                     }
119 
120                     if (i == headerLen) {
121                         // NAME (no value till the end of string)
122                         nameEnd = headerLen;
123                         valueBegin = valueEnd = -1;
124                         break;
125                     }
126                 }
127             }
128 
129             if (valueEnd > 0 && header.charAt(valueEnd - 1) == ',') {
130                 // old multiple cookies separator, skipping it
131                 valueEnd--;
132             }
133 
134             if (cookieBuilder == null) {
135                 // cookie name-value pair
136                 DefaultCookie cookie = initCookie(header, nameBegin, nameEnd, valueBegin, valueEnd);
137 
138                 if (cookie == null) {
139                     return null;
140                 }
141 
142                 cookieBuilder = new CookieBuilder(cookie);
143             } else {
144                 // cookie attribute
145                 String attrValue = valueBegin == -1 ? null : header.substring(valueBegin, valueEnd);
146                 cookieBuilder.appendAttribute(header, nameBegin, nameEnd, attrValue);
147             }
148         }
149         return cookieBuilder.cookie();
150     }
151 
152     private static class CookieBuilder {
153 
154         private final DefaultCookie cookie;
155         private String domain;
156         private String path;
157         private int maxAge = Integer.MIN_VALUE;
158         private String expires;
159         private boolean secure;
160         private boolean httpOnly;
161 
162         public CookieBuilder(DefaultCookie cookie) {
163             this.cookie = cookie;
164         }
165 
166         private int mergeMaxAgeAndExpire(int maxAge, String expires) {
167             // max age has precedence over expires
168             if (maxAge != Integer.MIN_VALUE) {
169                 return maxAge;
170             } else if (expires != null) {
171                 Date expiresDate = HttpHeaderDateFormat.get().parse(expires, new ParsePosition(0));
172                 if (expiresDate != null) {
173                     long maxAgeMillis = expiresDate.getTime() - System.currentTimeMillis();
174                     return (int) (maxAgeMillis / 1000 + (maxAgeMillis % 1000 != 0 ? 1 : 0));
175                 }
176             }
177             return Integer.MIN_VALUE;
178         }
179 
180         public Cookie cookie() {
181             cookie.setDomain(domain);
182             cookie.setPath(path);
183             cookie.setMaxAge(mergeMaxAgeAndExpire(maxAge, expires));
184             cookie.setSecure(secure);
185             cookie.setHttpOnly(httpOnly);
186             return cookie;
187         }
188 
189         /**
190          * Parse and store a key-value pair. First one is considered to be the
191          * cookie name/value. Unknown attribute names are silently discarded.
192          *
193          * @param header
194          *            the HTTP header
195          * @param keyStart
196          *            where the key starts in the header
197          * @param keyEnd
198          *            where the key ends in the header
199          * @param value
200          *            the decoded value
201          */
202         public void appendAttribute(String header, int keyStart, int keyEnd,
203                 String value) {
204             setCookieAttribute(header, keyStart, keyEnd, value);
205         }
206 
207         private void setCookieAttribute(String header, int keyStart,
208                 int keyEnd, String value) {
209             int length = keyEnd - keyStart;
210 
211             if (length == 4) {
212                 parse4(header, keyStart, value);
213             } else if (length == 6) {
214                 parse6(header, keyStart, value);
215             } else if (length == 7) {
216                 parse7(header, keyStart, value);
217             } else if (length == 8) {
218                 parse8(header, keyStart, value);
219             }
220         }
221 
222         private void parse4(String header, int nameStart, String value) {
223             if (header.regionMatches(true, nameStart, CookieHeaderNames.PATH, 0, 4)) {
224                 path = value;
225             }
226         }
227 
228         private void parse6(String header, int nameStart, String value) {
229             if (header.regionMatches(true, nameStart, CookieHeaderNames.DOMAIN, 0, 5)) {
230                 domain = value.length() > 0 ? value.toString() : null;
231             } else if (header.regionMatches(true, nameStart, CookieHeaderNames.SECURE, 0, 5)) {
232                 secure = true;
233             }
234         }
235 
236         private void setExpire(String value) {
237             expires = value;
238         }
239 
240         private void setMaxAge(String value) {
241             try {
242                 maxAge = Math.max(Integer.valueOf(value), 0);
243             } catch (NumberFormatException e1) {
244                 // ignore failure to parse -> treat as session cookie
245             }
246         }
247 
248         private void parse7(String header, int nameStart, String value) {
249             if (header.regionMatches(true, nameStart, CookieHeaderNames.EXPIRES, 0, 7)) {
250                 setExpire(value);
251             } else if (header.regionMatches(true, nameStart, CookieHeaderNames.MAX_AGE, 0, 7)) {
252                 setMaxAge(value);
253             }
254         }
255 
256         private void parse8(String header, int nameStart, String value) {
257             if (header.regionMatches(true, nameStart, CookieHeaderNames.HTTPONLY, 0, 8)) {
258                 httpOnly = true;
259             }
260         }
261     }
262 }