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