1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package io.netty5.handler.codec.http.cookie;
17
18 import io.netty5.handler.codec.DateFormatter;
19 import io.netty5.handler.codec.http.cookie.CookieHeaderNames.SameSite;
20
21 import java.util.Date;
22
23 import static java.util.Objects.requireNonNull;
24
25
26
27
28
29
30
31
32
33 public final class ClientCookieDecoder extends CookieDecoder {
34
35
36
37
38
39 public static final ClientCookieDecoder STRICT = new ClientCookieDecoder(true);
40
41
42
43
44 public static final ClientCookieDecoder LAX = new ClientCookieDecoder(false);
45
46 private ClientCookieDecoder(boolean strict) {
47 super(strict);
48 }
49
50
51
52
53
54
55 public Cookie decode(String header) {
56 final int headerLen = requireNonNull(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
67 for (;;) {
68 if (i == headerLen) {
69 break loop;
70 }
71 char c = header.charAt(i);
72 if (c == ',') {
73
74
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
94 nameEnd = i;
95 valueBegin = valueEnd = -1;
96 break;
97
98 } else if (curChar == '=') {
99
100 nameEnd = i;
101 i++;
102 if (i == headerLen) {
103
104 valueBegin = valueEnd = 0;
105 break;
106 }
107
108 valueBegin = i;
109
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
119 nameEnd = headerLen;
120 valueBegin = valueEnd = -1;
121 break;
122 }
123 }
124
125 if (valueEnd > 0 && header.charAt(valueEnd - 1) == ',') {
126
127 valueEnd--;
128 }
129
130 if (cookieBuilder == null) {
131
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
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
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
191
192
193
194
195
196
197
198
199
200
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
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 }