Skip to content

Commit 2c65aca

Browse files
dz-1stevehu
authored andcommitted
fix cookie tests - #97 (#100)
* fix cookie tests - #97 * enable test cases for cookie in parameter integration * add system property to pom * by pass undertow cookie parser * fixes #97 add copyright header for the new class * fixes #97 remove used comments
1 parent 8e50047 commit 2c65aca

File tree

19 files changed

+342
-39
lines changed

19 files changed

+342
-39
lines changed
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
/*
2+
* JBoss, Home of Professional Open Source.
3+
* Copyright 2014 Red Hat, Inc., and individual contributors
4+
* as indicated by the @author tags.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package com.networknt.openapi.parameter;
20+
21+
import java.util.HashMap;
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.TreeMap;
25+
26+
import io.undertow.UndertowLogger;
27+
import io.undertow.UndertowMessages;
28+
import io.undertow.server.handlers.Cookie;
29+
import io.undertow.server.handlers.CookieImpl;
30+
31+
/**
32+
* Adapted from io.undertow.util.Cookies to support comma delimited values.
33+
*
34+
* @author Daniel Zhao
35+
*
36+
*/
37+
38+
public class CookieHelper {
39+
40+
public static final String DOMAIN = "$Domain";
41+
public static final String VERSION = "$Version";
42+
public static final String PATH = "$Path";
43+
44+
private static final char[] HTTP_SEPARATORS;
45+
private static final boolean[] HTTP_SEPARATOR_FLAGS = new boolean[128];
46+
47+
/**
48+
* If set to true, the <code>/</code> character will be treated as a
49+
* separator. Default is false.
50+
*/
51+
private static final boolean FWD_SLASH_IS_SEPARATOR = Boolean.getBoolean("io.undertow.legacy.cookie.FWD_SLASH_IS_SEPARATOR");
52+
53+
static {
54+
/*
55+
Excluding the '/' char by default violates the RFC, but
56+
it looks like a lot of people put '/'
57+
in unquoted values: '/': ; //47
58+
'\t':9 ' ':32 '\"':34 '(':40 ')':41 ',':44 ':':58 ';':59 '<':60
59+
'=':61 '>':62 '?':63 '@':64 '[':91 '\\':92 ']':93 '{':123 '}':125
60+
*/
61+
if (FWD_SLASH_IS_SEPARATOR) {
62+
HTTP_SEPARATORS = new char[]{'\t', ' ', '\"', '(', ')', ',', '/',
63+
':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '{', '}'};
64+
} else {
65+
HTTP_SEPARATORS = new char[]{'\t', ' ', '\"', '(', ')', ',',
66+
':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '{', '}'};
67+
}
68+
for (int i = 0; i < 128; i++) {
69+
HTTP_SEPARATOR_FLAGS[i] = false;
70+
}
71+
for (char HTTP_SEPARATOR : HTTP_SEPARATORS) {
72+
HTTP_SEPARATOR_FLAGS[HTTP_SEPARATOR] = true;
73+
}
74+
}
75+
76+
77+
/**
78+
/**
79+
* Parses the cookies from a list of "Cookie:" header values. The cookie header values are parsed according to RFC2109 that
80+
* defines the following syntax:
81+
*
82+
* <pre>
83+
* <code>
84+
* cookie = "Cookie:" cookie-version
85+
* 1*((";" | ",") cookie-value)
86+
* cookie-value = NAME "=" VALUE [";" path] [";" domain]
87+
* cookie-version = "$Version" "=" value
88+
* NAME = attr
89+
* VALUE = value
90+
* path = "$Path" "=" value
91+
* domain = "$Domain" "=" value
92+
* </code>
93+
* </pre>
94+
*
95+
* @param maxCookies The maximum number of cookies. Used to prevent hash collision attacks
96+
* @param allowEqualInValue if true equal characters are allowed in cookie values
97+
* @param cookies The cookie values to parse
98+
* @return A pared cookie map
99+
*
100+
* @see Cookie
101+
* @see <a href="http://tools.ietf.org/search/rfc2109">rfc2109</a>
102+
*/
103+
public static Map<String, Cookie> parseRequestCookies(int maxCookies, boolean allowEqualInValue, List<String> cookies) {
104+
return parseRequestCookies(maxCookies, allowEqualInValue, cookies, false);
105+
}
106+
107+
static Map<String, Cookie> parseRequestCookies(int maxCookies, boolean allowEqualInValue, List<String> cookies, boolean commaIsSeperator) {
108+
return parseRequestCookies(maxCookies, allowEqualInValue, cookies, commaIsSeperator, true);
109+
}
110+
111+
static Map<String, Cookie> parseRequestCookies(int maxCookies, boolean allowEqualInValue, List<String> cookies, boolean commaIsSeperator, boolean allowHttpSepartorsV0) {
112+
if (cookies == null) {
113+
return new TreeMap<>();
114+
}
115+
final Map<String, Cookie> parsedCookies = new TreeMap<>();
116+
117+
for (String cookie : cookies) {
118+
parseCookie(cookie, parsedCookies, maxCookies, allowEqualInValue, commaIsSeperator, allowHttpSepartorsV0);
119+
}
120+
return parsedCookies;
121+
}
122+
123+
private static void parseCookie(final String cookie, final Map<String, Cookie> parsedCookies, int maxCookies, boolean allowEqualInValue, boolean commaIsSeperator, boolean allowHttpSepartorsV0) {
124+
int state = 0;
125+
String name = null;
126+
int start = 0;
127+
boolean containsEscapedQuotes = false;
128+
int cookieCount = parsedCookies.size();
129+
final Map<String, String> cookies = new HashMap<>();
130+
final Map<String, String> additional = new HashMap<>();
131+
for (int i = 0; i < cookie.length(); ++i) {
132+
char c = cookie.charAt(i);
133+
switch (state) {
134+
case 0: {
135+
//eat leading whitespace
136+
if (c == ' ' || c == '\t' || c == ';') {
137+
start = i + 1;
138+
break;
139+
}
140+
state = 1;
141+
//fall through
142+
}
143+
case 1: {
144+
//extract key
145+
if (c == '=') {
146+
name = cookie.substring(start, i);
147+
start = i + 1;
148+
state = 2;
149+
} else if (c == ';' || (commaIsSeperator && c == ',')) {
150+
if(name != null) {
151+
cookieCount = createCookie(name, cookie.substring(start, i), maxCookies, cookieCount, cookies, additional);
152+
} else if(UndertowLogger.REQUEST_LOGGER.isTraceEnabled()) {
153+
UndertowLogger.REQUEST_LOGGER.trace("Ignoring invalid cookies in header " + cookie);
154+
}
155+
state = 0;
156+
start = i + 1;
157+
}
158+
break;
159+
}
160+
case 2: {
161+
//extract value
162+
if (c == ';' || (commaIsSeperator && c == ',')) {
163+
cookieCount = createCookie(name, cookie.substring(start, i), maxCookies, cookieCount, cookies, additional);
164+
state = 0;
165+
start = i + 1;
166+
} else if (c == '"' && start == i) { //only process the " if it is the first character
167+
containsEscapedQuotes = false;
168+
state = 3;
169+
start = i + 1;
170+
} else if (c == '=') {
171+
if (!allowEqualInValue && !allowHttpSepartorsV0) {
172+
cookieCount = createCookie(name, cookie.substring(start, i), maxCookies, cookieCount, cookies, additional);
173+
state = 4;
174+
start = i + 1;
175+
}
176+
} else if (!allowHttpSepartorsV0 && isHttpSeparator(c)) {
177+
cookieCount = createCookie(name, cookie.substring(start, i), maxCookies, cookieCount, cookies, additional);
178+
state = 4;
179+
start = i + 1;
180+
}
181+
break;
182+
}
183+
case 3: {
184+
//extract quoted value
185+
if (c == '"') {
186+
cookieCount = createCookie(name, containsEscapedQuotes ? unescapeDoubleQuotes(cookie.substring(start, i)) : cookie.substring(start, i), maxCookies, cookieCount, cookies, additional);
187+
state = 0;
188+
start = i + 1;
189+
}
190+
// Skip the next double quote char '"' when it is escaped by backslash '\' (i.e. \") inside the quoted value
191+
if (c == '\\' && (i + 1 < cookie.length()) && cookie.charAt(i + 1) == '"') {
192+
// But..., do not skip at the following conditions
193+
if (i + 2 == cookie.length()) { // Cookie: key="\" or Cookie: key="...\"
194+
break;
195+
}
196+
if (i + 2 < cookie.length() && (cookie.charAt(i + 2) == ';' // Cookie: key="\"; key2=...
197+
|| (commaIsSeperator && cookie.charAt(i + 2) == ','))) { // Cookie: key="\", key2=...
198+
break;
199+
}
200+
// Skip the next double quote char ('"' behind '\') in the cookie value
201+
i++;
202+
containsEscapedQuotes = true;
203+
}
204+
break;
205+
}
206+
case 4: {
207+
//skip value portion behind '='
208+
if (c == ';' || (commaIsSeperator && c == ',')) {
209+
state = 0;
210+
}
211+
start = i + 1;
212+
break;
213+
}
214+
}
215+
}
216+
if (state == 2) {
217+
createCookie(name, cookie.substring(start), maxCookies, cookieCount, cookies, additional);
218+
}
219+
220+
for (final Map.Entry<String, String> entry : cookies.entrySet()) {
221+
Cookie c = new CookieImpl(entry.getKey(), entry.getValue());
222+
String domain = additional.get(DOMAIN);
223+
if (domain != null) {
224+
c.setDomain(domain);
225+
}
226+
String version = additional.get(VERSION);
227+
if (version != null) {
228+
c.setVersion(Integer.parseInt(version));
229+
}
230+
String path = additional.get(PATH);
231+
if (path != null) {
232+
c.setPath(path);
233+
}
234+
parsedCookies.put(c.getName(), c);
235+
}
236+
}
237+
238+
private static int createCookie(final String name, final String value, int maxCookies, int cookieCount,
239+
final Map<String, String> cookies, final Map<String, String> additional) {
240+
if (!name.isEmpty() && name.charAt(0) == '$') {
241+
if(additional.containsKey(name)) {
242+
return cookieCount;
243+
}
244+
additional.put(name, value);
245+
return cookieCount;
246+
} else {
247+
if (cookieCount == maxCookies) {
248+
throw UndertowMessages.MESSAGES.tooManyCookies(maxCookies);
249+
}
250+
if(cookies.containsKey(name)) {
251+
return cookieCount;
252+
}
253+
cookies.put(name, value);
254+
return ++cookieCount;
255+
}
256+
}
257+
258+
private static String unescapeDoubleQuotes(final String value) {
259+
if (value == null || value.isEmpty()) {
260+
return value;
261+
}
262+
263+
// Replace all escaped double quote (\") to double quote (")
264+
char[] tmp = new char[value.length()];
265+
int dest = 0;
266+
for(int i = 0; i < value.length(); i++) {
267+
if (value.charAt(i) == '\\' && (i + 1 < value.length()) && value.charAt(i + 1) == '"') {
268+
i++;
269+
}
270+
tmp[dest] = value.charAt(i);
271+
dest++;
272+
}
273+
return new String(tmp, 0, dest);
274+
}
275+
276+
/**
277+
* Returns true if the byte is a separator as defined by V1 of the cookie
278+
* spec, RFC2109.
279+
* @throws IllegalArgumentException if a control character was supplied as
280+
* input
281+
*/
282+
static boolean isHttpSeparator(final char c) {
283+
if (c < 0x20 || c >= 0x7f) {
284+
if (c != 0x09) {
285+
throw UndertowMessages.MESSAGES.invalidControlCharacter(Integer.toString(c));
286+
}
287+
}
288+
289+
return HTTP_SEPARATOR_FLAGS[c];
290+
}
291+
}
292+
293+

openapi-meta/src/main/java/com/networknt/openapi/parameter/CookieParameterDeserializer.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
import com.networknt.openapi.OpenApiHandler;
1010
import com.networknt.utility.StringUtils;
1111

12+
import io.undertow.UndertowOptions;
1213
import io.undertow.server.HttpServerExchange;
1314
import io.undertow.server.handlers.Cookie;
1415
import io.undertow.util.AttachmentKey;
16+
import io.undertow.util.Headers;
1517

1618
public class CookieParameterDeserializer implements ParameterDeserializer {
1719
private static final String FORM="form";
@@ -36,7 +38,17 @@ public boolean isApplicable(ValueType valueType, boolean exploade) {
3638
@Override
3739
public Object deserialize(HttpServerExchange exchange, Parameter parameter, ValueType valueType,
3840
boolean exploade) {
39-
Cookie cookie = exchange.getRequestCookies().get(parameter.getName());
41+
List<String> rawCookies = exchange.getRequestHeaders().get(Headers.COOKIE);
42+
43+
44+
Map<String, Cookie> cookies = CookieHelper.parseRequestCookies(
45+
exchange.getConnection().getUndertowOptions().get(UndertowOptions.MAX_COOKIES, 200),
46+
exchange.getConnection().getUndertowOptions().get(UndertowOptions.ALLOW_EQUALS_IN_COOKIE_VALUE, false),
47+
rawCookies);
48+
49+
50+
Cookie cookie = cookies.get(parameter.getName());
51+
4052
String value = cookie.getValue();
4153

4254
if (ValueType.ARRAY == valueType) {

openapi-meta/src/test/java/com/networknt/openapi/parameter/IntegrationTest.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -408,15 +408,14 @@ public void test_object_simple_no_explode_header_param_deserialization() throws
408408
runTest("/pets_header_obj_no_ep", EXPECTED_MAP_RESULT, headers, Collections.emptyMap());
409409
}
410410

411-
//@Test
411+
@Test
412412
public void test_array_cookie_param_deserialization() throws Exception {
413413
Map<String, String> cookies = new HashMap<>();
414414
cookies.put("petId", "3,4,5");
415-
416415
runTest("/pets_cookie_array", EXPECTED_ARRAY_RESULT, Collections.emptyMap(), cookies);
417416
}
418417

419-
//@Test
418+
@Test
420419
public void test_object_simple_no_explode_cookie_param_deserialization() throws Exception {
421420
Map<String, String> cookies = new HashMap<>();
422421
cookies.put("petId", "id,001,name,Dog");
885 Bytes
Binary file not shown.

openapi-meta/src/test/resources/logback-test.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,15 @@
5757
</triggeringPolicy>
5858
</appender>
5959

60-
<root level="trace">
60+
<root level="info">
6161
<appender-ref ref="stdout" />
6262
</root>
6363

64-
<logger name="com.networknt" level="trace">
64+
<logger name="com.networknt" level="info">
6565
<appender-ref ref="log"/>
6666
</logger>
6767

68-
<logger name="Audit" level="trace" additivity="false">
68+
<logger name="Audit" level="info" additivity="false">
6969
<appender-ref ref="audit"/>
7070
</logger>
7171

885 Bytes
Binary file not shown.

openapi-security/src/test/resources/logback-test.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,11 @@
6161
<appender-ref ref="stdout" />
6262
</root>
6363

64-
<logger name="com.networknt.config" level="debug">
64+
<logger name="com.networknt.config" level="info">
6565
<appender-ref ref="log"/>
6666
</logger>
6767

68-
<logger name="Audit" level="trace" additivity="false">
68+
<logger name="Audit" level="info" additivity="false">
6969
<appender-ref ref="audit"/>
7070
</logger>
7171

openapi-validator/src/test/java/com/networknt/openapi/RequestValidatorTest.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -417,15 +417,14 @@ public void test_object_simple_no_explode_header_param_deserialization() throws
417417
runTest("/pets_header_obj_no_ep", EXPECTED_MAP_RESULT, headers, Collections.emptyMap());
418418
}
419419

420-
//@Test
420+
@Test
421421
public void test_array_cookie_param_deserialization() throws Exception {
422422
Map<String, String> cookies = new HashMap<>();
423423
cookies.put("petId", "3,4,5");
424-
425424
runTest("/pets_cookie_array", EXPECTED_ARRAY_RESULT, Collections.emptyMap(), cookies);
426425
}
427426

428-
//@Test
427+
@Test
429428
public void test_object_simple_no_explode_cookie_param_deserialization() throws Exception {
430429
Map<String, String> cookies = new HashMap<>();
431430
cookies.put("petId", "id,001,name,Dog");
885 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)