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