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 import io.netty.handler.codec.http.cookie.CookieHeaderNames.SameSite;
20
21 import java.util.Date;
22
23 import static io.netty.util.internal.ObjectUtil.checkNotNull;
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;
87             int valueBegin;
88             int valueEnd;
89
90             for (;;) {
91                 char curChar = header.charAt(i);
92                 if (curChar == ';') {
93                     // NAME; (no value till ';')
94                     nameEnd = i;
95                     valueBegin = valueEnd = -1;
96                     break;
97
98                 } else if (curChar == '=') {
99                     // NAME=VALUE
100                     nameEnd = i;
101                     i++;
102                     if (i == headerLen) {
103                         // NAME= (empty value, i.e. nothing after '=')
104                         valueBegin = valueEnd = 0;
105                         break;
106                     }
107
108                     valueBegin = i;
109                     // NAME=VALUE;
110                     int semiPos = header.indexOf(';', i);
111                     valueEnd = i = semiPos > 0 ? semiPos : headerLen;
112                     break;
113                 } else {
114                     i++;
115                 }
116
117                 if (i == headerLen) {
118                     // NAME (no value till the end of string)
119                     nameEnd = headerLen;
120                     valueBegin = valueEnd = -1;
121                     break;
122                 }
123             }
124
125             if (valueEnd > 0 && header.charAt(valueEnd - 1) == ',') {
126                 // old multiple cookies separator, skipping it
127                 valueEnd--;
128             }
129
130             if (cookieBuilder == null) {
131                 // cookie name-value pair
132                 DefaultCookie cookie = initCookie(header, nameBegin, nameEnd, valueBegin, valueEnd);
133
134                 if (cookie == null) {
135                     return null;
136                 }
137
138                 cookieBuilder = new CookieBuilder(cookie, header);
139             } else {
140                 // cookie attribute
141                 cookieBuilder.appendAttribute(nameBegin, nameEnd, valueBegin, valueEnd);
142             }
143         }
144         return cookieBuilder != null ? cookieBuilder.cookie() : null;
145     }
146
147     private static class CookieBuilder {
148
149         private final String header;
150         private final DefaultCookie cookie;
151         private String domain;
152         private String path;
153         private long maxAge = Long.MIN_VALUE;
154         private int expiresStart;
155         private int expiresEnd;
156         private boolean secure;
157         private boolean httpOnly;
158         private SameSite sameSite;
159
160         CookieBuilder(DefaultCookie cookie, String header) {
161             this.cookie = cookie;
162             this.header = header;
163         }
164
165         private long mergeMaxAgeAndExpires() {
166             // max age has precedence over expires
167             if (maxAge != Long.MIN_VALUE) {
168                 return maxAge;
169             } else if (isValueDefined(expiresStart, expiresEnd)) {
170                 Date expiresDate = DateFormatter.parseHttpDate(header, expiresStart, expiresEnd);
171                 if (expiresDate != null) {
172                     long maxAgeMillis = expiresDate.getTime() - System.currentTimeMillis();
173                     return maxAgeMillis / 1000 + (maxAgeMillis % 1000 != 0 ? 1 : 0);
174                 }
175             }
176             return Long.MIN_VALUE;
177         }
178
179         Cookie cookie() {
180             cookie.setDomain(domain);
181             cookie.setPath(path);
182             cookie.setMaxAge(mergeMaxAgeAndExpires());
183             cookie.setSecure(secure);
184             cookie.setHttpOnly(httpOnly);
185             cookie.setSameSite(sameSite);
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 keyStart
194          *            where the key starts in the header
195          * @param keyEnd
196          *            where the key ends in the header
197          * @param valueStart
198          *            where the value starts in the header
199          * @param valueEnd
200          *            where the value ends in the header
201          */

202         void appendAttribute(int keyStart, int keyEnd, int valueStart, int valueEnd) {
203             int length = keyEnd - keyStart;
204
205             if (length == 4) {
206                 parse4(keyStart, valueStart, valueEnd);
207             } else if (length == 6) {
208                 parse6(keyStart, valueStart, valueEnd);
209             } else if (length == 7) {
210                 parse7(keyStart, valueStart, valueEnd);
211             } else if (length == 8) {
212                 parse8(keyStart, valueStart, valueEnd);
213             }
214         }
215
216         private void parse4(int nameStart, int valueStart, int valueEnd) {
217             if (header.regionMatches(true, nameStart, CookieHeaderNames.PATH, 0, 4)) {
218                 path = computeValue(valueStart, valueEnd);
219             }
220         }
221
222         private void parse6(int nameStart, int valueStart, int valueEnd) {
223             if (header.regionMatches(true, nameStart, CookieHeaderNames.DOMAIN, 0, 5)) {
224                 domain = computeValue(valueStart, valueEnd);
225             } else if (header.regionMatches(true, nameStart, CookieHeaderNames.SECURE, 0, 5)) {
226                 secure = true;
227             }
228         }
229
230         private void setMaxAge(String value) {
231             try {
232                 maxAge = Math.max(Long.parseLong(value), 0L);
233             } catch (NumberFormatException e1) {
234                 // ignore failure to parse -> treat as session cookie
235             }
236         }
237
238         private void parse7(int nameStart, int valueStart, int valueEnd) {
239             if (header.regionMatches(true, nameStart, CookieHeaderNames.EXPIRES, 0, 7)) {
240                 expiresStart = valueStart;
241                 expiresEnd = valueEnd;
242             } else if (header.regionMatches(true, nameStart, CookieHeaderNames.MAX_AGE, 0, 7)) {
243                 setMaxAge(computeValue(valueStart, valueEnd));
244             }
245         }
246
247         private void parse8(int nameStart, int valueStart, int valueEnd) {
248             if (header.regionMatches(true, nameStart, CookieHeaderNames.HTTPONLY, 0, 8)) {
249                 httpOnly = true;
250             } else if (header.regionMatches(true, nameStart, CookieHeaderNames.SAMESITE, 0, 8)) {
251                 sameSite = SameSite.of(computeValue(valueStart, valueEnd));
252             }
253         }
254
255         private static boolean isValueDefined(int valueStart, int valueEnd) {
256             return valueStart != -1 && valueStart != valueEnd;
257         }
258
259         private String computeValue(int valueStart, int valueEnd) {
260             return isValueDefined(valueStart, valueEnd) ? header.substring(valueStart, valueEnd) : null;
261         }
262     }
263 }
264