diff --git a/codequality/checkstyle_suppressions.xml b/codequality/checkstyle_suppressions.xml
index 952267f908..09fe1d7c15 100644
--- a/codequality/checkstyle_suppressions.xml
+++ b/codequality/checkstyle_suppressions.xml
@@ -20,6 +20,8 @@
+
+
diff --git a/components/api/src/main/java/com/hotels/styx/api/ClientCookieDecoder.java b/components/api/src/main/java/com/hotels/styx/api/ClientCookieDecoder.java
new file mode 100644
index 0000000000..ef8cf86aa7
--- /dev/null
+++ b/components/api/src/main/java/com/hotels/styx/api/ClientCookieDecoder.java
@@ -0,0 +1,349 @@
+/*
+ * Copyright 2015 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package com.hotels.styx.api;
+
+import io.netty.handler.codec.DateFormatter;
+import io.netty.handler.codec.http.cookie.ClientCookieEncoder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.CharBuffer;
+import java.util.Date;
+
+import static com.hotels.styx.api.CookieUtil.firstInvalidCookieNameOctet;
+import static com.hotels.styx.api.CookieUtil.firstInvalidCookieValueOctet;
+import static com.hotels.styx.api.CookieUtil.unwrapValue;
+import static io.netty.util.internal.ObjectUtil.checkNotNull;
+
+/**
+ * A RFC6265 compliant cookie decoder to be used client side.
+ *
+ * It will store the way the raw value was wrapped in {@link NettyCookie#setWrap(boolean)} so it can be
+ * eventually sent back to the Origin server as is.
+ *
+ * @see ClientCookieEncoder
+ */
+final class ClientCookieDecoder {
+
+ private static final Logger logger = LoggerFactory.getLogger(ClientCookieDecoder.class);
+ /**
+ * Strict encoder that validates that name and value chars are in the valid scope
+ * defined in RFC6265
+ */
+ static final ClientCookieDecoder STRICT = new ClientCookieDecoder(true);
+
+ /**
+ * Lax instance that doesn't validate name and value
+ */
+ static final ClientCookieDecoder LAX = new ClientCookieDecoder(false);
+ private final boolean strict;
+
+
+ private ClientCookieDecoder(boolean strict) {
+ this.strict = strict;
+ }
+
+ /**
+ * Decodes the specified Set-Cookie HTTP header value into a {@link NettyCookie}.
+ *
+ * @return the decoded {@link NettyCookie}
+ */
+ NettyCookie decode(String header) {
+ final int headerLen = checkNotNull(header, "header").length();
+
+ if (headerLen == 0) {
+ return null;
+ }
+
+ CookieBuilder cookieBuilder = null;
+
+ loop:
+ for (int i = 0; ; ) {
+
+ // Skip spaces and separators.
+ for (; ; ) {
+ if (i == headerLen) {
+ break loop;
+ }
+ char c = header.charAt(i);
+ if (c == ',') {
+ // Having multiple cookies in a single Set-Cookie header is
+ // deprecated, modern browsers only parse the first one
+ break loop;
+
+ } else if (c == '\t' || c == '\n' || c == 0x0b || c == '\f'
+ || c == '\r' || c == ' ' || c == ';') {
+ i++;
+ continue;
+ }
+ break;
+ }
+
+ int nameBegin = i;
+ int nameEnd;
+ int valueBegin;
+ int valueEnd;
+
+ for (; ; ) {
+ char curChar = header.charAt(i);
+ if (curChar == ';') {
+ // NAME; (no value till ';')
+ nameEnd = i;
+ valueBegin = valueEnd = -1;
+ break;
+
+ } else if (curChar == '=') {
+ // NAME=VALUE
+ nameEnd = i;
+ i++;
+ if (i == headerLen) {
+ // NAME= (empty value, i.e. nothing after '=')
+ valueBegin = valueEnd = 0;
+ break;
+ }
+
+ valueBegin = i;
+ // NAME=VALUE;
+ int semiPos = header.indexOf(';', i);
+ valueEnd = i = semiPos > 0 ? semiPos : headerLen;
+ break;
+ } else {
+ i++;
+ }
+
+ if (i == headerLen) {
+ // NAME (no value till the end of string)
+ nameEnd = headerLen;
+ valueBegin = valueEnd = -1;
+ break;
+ }
+ }
+
+ if (valueEnd > 0 && header.charAt(valueEnd - 1) == ',') {
+ // old multiple cookies separator, skipping it
+ valueEnd--;
+ }
+
+ if (cookieBuilder == null) {
+ // cookie name-value pair
+ NettyCookie cookie = initCookie(header, nameBegin, nameEnd, valueBegin, valueEnd);
+ if (nameBegin == -1 || nameBegin == nameEnd) {
+ logger.debug("Skipping cookie with null name");
+ } else if (valueBegin == -1) {
+ logger.debug("Skipping cookie with null value");
+ } else {
+ CharSequence wrappedValue = CharBuffer.wrap(header, valueBegin, valueEnd);
+ CharSequence unwrappedValue = unwrapValue(wrappedValue);
+ if (unwrappedValue == null) {
+ logger.debug("Skipping cookie because starting quotes are not properly balanced in '{}'",
+ wrappedValue);
+ } else {
+ final String name = header.substring(nameBegin, nameEnd);
+ int invalidOctetPos;
+ if (strict && (invalidOctetPos = firstInvalidCookieNameOctet(name)) >= 0) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Skipping cookie because name '{}' contains invalid char '{}'",
+ name, name.charAt(invalidOctetPos));
+ }
+ } else {
+ final boolean wrap = unwrappedValue.length() != valueEnd - valueBegin;
+ if (strict && (invalidOctetPos = firstInvalidCookieValueOctet(unwrappedValue)) >= 0) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Skipping cookie because value '{}' contains invalid char '{}'",
+ unwrappedValue, unwrappedValue.charAt(invalidOctetPos));
+ }
+ } else {
+ NettyCookie cookie1 = new NettyCookie(name, unwrappedValue.toString());
+ cookie1.setWrap(wrap);
+ cookie = cookie1;
+ }
+ }
+ }
+ }
+
+ if (cookie == null) {
+ return null;
+ }
+
+ cookieBuilder = new CookieBuilder(cookie, header);
+ } else {
+ // cookie attribute
+ cookieBuilder.appendAttribute(nameBegin, nameEnd, valueBegin, valueEnd);
+ }
+ }
+ return cookieBuilder != null ? cookieBuilder.cookie() : null;
+ }
+
+ protected NettyCookie initCookie(String header, int nameBegin, int nameEnd, int valueBegin, int valueEnd) {
+ if (nameBegin == -1 || nameBegin == nameEnd) {
+ logger.debug("Skipping cookie with null name");
+ return null;
+ }
+
+ if (valueBegin == -1) {
+ logger.debug("Skipping cookie with null value");
+ return null;
+ }
+
+ CharSequence wrappedValue = CharBuffer.wrap(header, valueBegin, valueEnd);
+ CharSequence unwrappedValue = unwrapValue(wrappedValue);
+ if (unwrappedValue == null) {
+ logger.debug("Skipping cookie because starting quotes are not properly balanced in '{}'",
+ wrappedValue);
+ return null;
+ }
+
+ final String name = header.substring(nameBegin, nameEnd);
+
+ int invalidOctetPos;
+ if (strict && (invalidOctetPos = firstInvalidCookieNameOctet(name)) >= 0) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Skipping cookie because name '{}' contains invalid char '{}'",
+ name, name.charAt(invalidOctetPos));
+ }
+ return null;
+ }
+
+ final boolean wrap = unwrappedValue.length() != valueEnd - valueBegin;
+
+ if (strict && (invalidOctetPos = firstInvalidCookieValueOctet(unwrappedValue)) >= 0) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Skipping cookie because value '{}' contains invalid char '{}'",
+ unwrappedValue, unwrappedValue.charAt(invalidOctetPos));
+ }
+ return null;
+ }
+
+ NettyCookie cookie = new NettyCookie(name, unwrappedValue.toString());
+ cookie.setWrap(wrap);
+ return cookie;
+ }
+
+
+ private static class CookieBuilder {
+
+ private final String header;
+ private final NettyCookie cookie;
+ private String domain;
+ private String path;
+ private long maxAge = Long.MIN_VALUE;
+ private int expiresStart;
+ private int expiresEnd;
+ private boolean secure;
+ private boolean httpOnly;
+ private String sameSite;
+
+ CookieBuilder(NettyCookie cookie, String header) {
+ this.cookie = cookie;
+ this.header = header;
+ }
+
+ private long mergeMaxAgeAndExpires() {
+ // max age has precedence over expires
+ if (maxAge != Long.MIN_VALUE) {
+ return maxAge;
+ } else if (isValueDefined(expiresStart, expiresEnd)) {
+ Date expiresDate = DateFormatter.parseHttpDate(header, expiresStart, expiresEnd);
+ if (expiresDate != null) {
+ long maxAgeMillis = expiresDate.getTime() - System.currentTimeMillis();
+ return maxAgeMillis / 1000 + (maxAgeMillis % 1000 != 0 ? 1 : 0);
+ }
+ }
+ return Long.MIN_VALUE;
+ }
+
+ NettyCookie cookie() {
+ cookie.setDomain(domain);
+ cookie.setPath(path);
+ cookie.setMaxAge(mergeMaxAgeAndExpires());
+ cookie.setSecure(secure);
+ cookie.setHttpOnly(httpOnly);
+ cookie.setSameSite(sameSite);
+ return cookie;
+ }
+
+ /**
+ * Parse and store a key-value pair. First one is considered to be the
+ * cookie name/value. Unknown attribute names are silently discarded.
+ *
+ * @param keyStart where the key starts in the header
+ * @param keyEnd where the key ends in the header
+ * @param valueStart where the value starts in the header
+ * @param valueEnd where the value ends in the header
+ */
+ void appendAttribute(int keyStart, int keyEnd, int valueStart, int valueEnd) {
+ int length = keyEnd - keyStart;
+
+ if (length == 4) {
+ parse4(keyStart, valueStart, valueEnd);
+ } else if (length == 6) {
+ parse6(keyStart, valueStart, valueEnd);
+ } else if (length == 7) {
+ parse7(keyStart, valueStart, valueEnd);
+ } else if (length == 8) {
+ parse8(keyStart, valueStart, valueEnd);
+ }
+ }
+
+ private void parse4(int nameStart, int valueStart, int valueEnd) {
+ if (header.regionMatches(true, nameStart, CookieHeaderNames.PATH, 0, 4)) {
+ path = computeValue(valueStart, valueEnd);
+ }
+ }
+
+ private void parse6(int nameStart, int valueStart, int valueEnd) {
+ if (header.regionMatches(true, nameStart, CookieHeaderNames.DOMAIN, 0, 5)) {
+ domain = computeValue(valueStart, valueEnd);
+ } else if (header.regionMatches(true, nameStart, CookieHeaderNames.SECURE, 0, 5)) {
+ secure = true;
+ }
+ }
+
+ private void setMaxAge(String value) {
+ try {
+ maxAge = Math.max(Long.parseLong(value), 0L);
+ } catch (NumberFormatException e1) {
+ // ignore failure to parse -> treat as session cookie
+ }
+ }
+
+ private void parse7(int nameStart, int valueStart, int valueEnd) {
+ if (header.regionMatches(true, nameStart, CookieHeaderNames.EXPIRES, 0, 7)) {
+ expiresStart = valueStart;
+ expiresEnd = valueEnd;
+ } else if (header.regionMatches(true, nameStart, CookieHeaderNames.MAX_AGE, 0, 7)) {
+ setMaxAge(computeValue(valueStart, valueEnd));
+ }
+ }
+
+ private void parse8(int nameStart, int valueStart, int valueEnd) {
+ if (header.regionMatches(true, nameStart, CookieHeaderNames.HTTPONLY, 0, 8)) {
+ httpOnly = true;
+ } else if (header.regionMatches(true, nameStart, CookieHeaderNames.SAMESITE, 0, 8)) {
+ sameSite = computeValue(valueStart, valueEnd);
+ }
+ }
+
+
+ private static boolean isValueDefined(int valueStart, int valueEnd) {
+ return valueStart != -1 && valueStart != valueEnd;
+ }
+
+ private String computeValue(int valueStart, int valueEnd) {
+ return isValueDefined(valueStart, valueEnd) ? header.substring(valueStart, valueEnd) : null;
+ }
+ }
+}
diff --git a/components/api/src/main/java/com/hotels/styx/api/CookieHeaderNames.java b/components/api/src/main/java/com/hotels/styx/api/CookieHeaderNames.java
new file mode 100644
index 0000000000..6391d17b52
--- /dev/null
+++ b/components/api/src/main/java/com/hotels/styx/api/CookieHeaderNames.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2015 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package com.hotels.styx.api;
+
+final class CookieHeaderNames {
+ static final String PATH = "Path";
+
+ static final String EXPIRES = "Expires";
+
+ static final String MAX_AGE = "Max-Age";
+
+ static final String DOMAIN = "Domain";
+
+ static final String SECURE = "Secure";
+
+ static final String HTTPONLY = "HTTPOnly";
+
+ static final String SAMESITE = "SameSite";
+
+ private CookieHeaderNames() {
+ // Unused.
+ }
+
+}
diff --git a/components/api/src/main/java/com/hotels/styx/api/HttpHeaders.java b/components/api/src/main/java/com/hotels/styx/api/HttpHeaders.java
index 2511f2a65b..ed8e503ec3 100644
--- a/components/api/src/main/java/com/hotels/styx/api/HttpHeaders.java
+++ b/components/api/src/main/java/com/hotels/styx/api/HttpHeaders.java
@@ -1,5 +1,5 @@
/*
- Copyright (C) 2013-2018 Expedia Inc.
+ Copyright (C) 2013-2020 Expedia Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -142,11 +142,11 @@ public static class Builder {
private final DefaultHttpHeaders nettyHeaders;
public Builder() {
- this.nettyHeaders = new DefaultHttpHeaders(false);
+ this.nettyHeaders = new DefaultHttpHeaders(true);
}
public Builder(HttpHeaders headers) {
- this.nettyHeaders = new DefaultHttpHeaders(false);
+ this.nettyHeaders = new DefaultHttpHeaders(true);
this.nettyHeaders.set(headers.nettyHeaders);
}
diff --git a/components/api/src/main/java/com/hotels/styx/api/HttpVersion.java b/components/api/src/main/java/com/hotels/styx/api/HttpVersion.java
index 69a4d6f393..f9b17bca9b 100644
--- a/components/api/src/main/java/com/hotels/styx/api/HttpVersion.java
+++ b/components/api/src/main/java/com/hotels/styx/api/HttpVersion.java
@@ -1,5 +1,5 @@
/*
- Copyright (C) 2013-2018 Expedia Inc.
+ Copyright (C) 2013-2020 Expedia Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -40,12 +40,12 @@ private HttpVersion(String version) {
/**
* Creates a HttpVersion from String.
- *
+ *
* Accepted strings are "HTTP/1.0" and "HTTP/1.1".
* Otherwise throws an {@link IllegalArgumentException}.
*
* @param version
- * @return
+ * @return HttpVersion for the received version
*/
public static HttpVersion httpVersion(String version) {
checkArgument(VERSIONS.containsKey(version), "No such HTTP version %s", version);
diff --git a/components/api/src/main/java/com/hotels/styx/api/NettyCookie.java b/components/api/src/main/java/com/hotels/styx/api/NettyCookie.java
new file mode 100644
index 0000000000..d505f3cc05
--- /dev/null
+++ b/components/api/src/main/java/com/hotels/styx/api/NettyCookie.java
@@ -0,0 +1,76 @@
+/*
+ Copyright (C) 2013-2020 Expedia Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package com.hotels.styx.api;
+
+import io.netty.handler.codec.http.cookie.DefaultCookie;
+
+import static com.hotels.styx.api.CookieUtil.stringBuilder;
+
+
+class NettyCookie extends DefaultCookie {
+
+
+ private String sameSite;
+ /**
+ * Creates a new cookie with the specified name and value.
+ *
+ * @param name
+ * @param value
+ */
+ public NettyCookie(String name, String value) {
+ super(name, value);
+ }
+
+
+ public String sameSite() {
+ return sameSite;
+ }
+
+ public void setSameSite(String sameSite) {
+ this.sameSite = sameSite;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder buf = stringBuilder()
+ .append(name())
+ .append('=')
+ .append(value());
+ if (domain() != null) {
+ buf.append(", domain=")
+ .append(domain());
+ }
+ if (path() != null) {
+ buf.append(", path=")
+ .append(path());
+ }
+ if (maxAge() != 0) {
+ buf.append(", maxAge=")
+ .append(maxAge())
+ .append('s');
+ }
+ if (isSecure()) {
+ buf.append(", secure");
+ }
+ if (isHttpOnly()) {
+ buf.append(", HTTPOnly");
+ }
+ if (sameSite != null) {
+ buf.append(", SameSite=").append(sameSite.toString());
+ }
+ return buf.toString();
+ }
+}
diff --git a/components/api/src/main/java/com/hotels/styx/api/ResponseCookie.java b/components/api/src/main/java/com/hotels/styx/api/ResponseCookie.java
index a2601a0c47..91c2e2c2cf 100644
--- a/components/api/src/main/java/com/hotels/styx/api/ResponseCookie.java
+++ b/components/api/src/main/java/com/hotels/styx/api/ResponseCookie.java
@@ -15,9 +15,6 @@
*/
package com.hotels.styx.api;
-import io.netty.handler.codec.http.cookie.ClientCookieDecoder;
-import io.netty.handler.codec.http.cookie.Cookie;
-import io.netty.handler.codec.http.cookie.DefaultCookie;
import java.util.Collection;
import java.util.List;
@@ -33,7 +30,7 @@
/**
* Represents an HTTP cookie as sent in the HTTP response {@code Set-Cookie} header.
- *
+ *
* A server can include a {@code ResponseCookie} in its response to a client request.
* It contains cookie {@code name}, {@code value}, and attributes such as {@code path}
* and {@code maxAge}.
@@ -49,6 +46,13 @@ public final class ResponseCookie {
private final boolean httpOnly;
private final boolean secure;
private final int hashCode;
+ private final String sameSite;
+
+ public enum SameSite {
+ Lax,
+ Strict,
+ None
+ }
private ResponseCookie(Builder builder) {
if (builder.name == null || builder.name.isEmpty()) {
@@ -63,7 +67,8 @@ private ResponseCookie(Builder builder) {
this.path = builder.path;
this.httpOnly = builder.httpOnly;
this.secure = builder.secure;
- this.hashCode = hash(name, value, domain, maxAge, path, secure, httpOnly);
+ this.sameSite = builder.sameSite;
+ this.hashCode = hash(name, value, domain, maxAge, path, secure, httpOnly, sameSite);
}
/**
@@ -98,7 +103,7 @@ public static Set decode(List headerValues) {
* @return "Set-Cookie" header values
*/
public static List encode(Collection cookies) {
- Set nettyCookies = cookies.stream()
+ Set nettyCookies = cookies.stream()
.map(ResponseCookie::convert)
.collect(toSet());
@@ -178,8 +183,18 @@ public boolean secure() {
return secure;
}
- private static Cookie convert(ResponseCookie cookie) {
- DefaultCookie nCookie = new DefaultCookie(cookie.name, cookie.value);
+ /**
+ * Returns the SameSite attribute, if present.
+ *
+ * @return SameSite attribute, if present
+ */
+ public Optional sameSite() {
+ return Optional.ofNullable(sameSite);
+ }
+
+
+ private static NettyCookie convert(ResponseCookie cookie) {
+ NettyCookie nCookie = new NettyCookie(cookie.name, cookie.value);
nCookie.setDomain(cookie.domain);
nCookie.setHttpOnly(cookie.httpOnly);
@@ -188,11 +203,12 @@ private static Cookie convert(ResponseCookie cookie) {
nCookie.setMaxAge(cookie.maxAge);
}
nCookie.setPath(cookie.path);
+ nCookie.setSameSite(cookie.sameSite);
return nCookie;
}
- private static ResponseCookie convert(Cookie cookie) {
+ private static ResponseCookie convert(NettyCookie cookie) {
String value = cookie.wrap() ? quote(cookie.value()) : cookie.value();
return responseCookie(cookie.name(), value)
@@ -201,6 +217,7 @@ private static ResponseCookie convert(Cookie cookie) {
.maxAge(cookie.maxAge())
.httpOnly(cookie.isHttpOnly())
.secure(cookie.isSecure())
+ .sameSiteRawValue(cookie.sameSite())
.build();
}
@@ -223,7 +240,8 @@ public boolean equals(Object o) {
&& Objects.equals(value, that.value)
&& Objects.equals(domain, that.domain)
&& Objects.equals(maxAge, that.maxAge)
- && Objects.equals(path, that.path);
+ && Objects.equals(path, that.path)
+ && Objects.equals(sameSite, that.sameSite);
}
@Override
@@ -241,6 +259,7 @@ public String toString() {
+ ", path='" + path + '\''
+ ", httpOnly=" + httpOnly
+ ", secure=" + secure
+ + ", sameSite=" + sameSite
+ '}';
}
@@ -257,6 +276,7 @@ public static class Builder {
private String path;
private boolean httpOnly;
private boolean secure;
+ private String sameSite;
private Builder(String name, String value) {
this.name = requireNonNull(name);
@@ -340,6 +360,28 @@ public Builder secure(boolean secure) {
return this;
}
+ /**
+ * Sets/unsets the SameSite attribute.
+ *
+ * @param sameSite enum with a valid value for the SameSite attribute
+ * @return this builder
+ */
+ public Builder sameSite(SameSite sameSite) {
+ this.sameSite = sameSite.name();
+ return this;
+ }
+
+ /**
+ * Sets/unsets the SameSite attribute.
+ * @param sameSite SameSite attribute
+ * @return this builder
+ */
+ public Builder sameSiteRawValue(String sameSite) {
+ this.sameSite = sameSite;
+ return this;
+ }
+
+
public ResponseCookie build() {
return new ResponseCookie(this);
}
diff --git a/components/api/src/main/java/com/hotels/styx/api/ServerCookieEncoder.java b/components/api/src/main/java/com/hotels/styx/api/ServerCookieEncoder.java
index 06b95cf8ee..d2b3a4b05d 100644
--- a/components/api/src/main/java/com/hotels/styx/api/ServerCookieEncoder.java
+++ b/components/api/src/main/java/com/hotels/styx/api/ServerCookieEncoder.java
@@ -17,10 +17,7 @@
import io.netty.handler.codec.http.HttpHeaderDateFormat;
import io.netty.handler.codec.http.HttpRequest;
-import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.CookieEncoder;
-import io.netty.handler.codec.http.cookie.CookieHeaderNames;
-import io.netty.handler.codec.http.cookie.DefaultCookie;
import java.util.ArrayList;
import java.util.Collection;
@@ -40,9 +37,9 @@
/**
* A RFC6265 compliant cookie encoder to be used server side,
* so some fields are sent (Version is typically ignored).
- *
+ *
* As Netty's Cookie merges Expires and MaxAge into one single field, only Max-Age field is sent.
- *
+ *
* Note that multiple cookies are supposed to be sent at once in a single "Set-Cookie" header.
*
*
@@ -50,7 +47,6 @@
* {@link HttpRequest} req = ...;
* res.setHeader("Cookie", {@link ServerCookieEncoder}.encode("JSESSIONID", "1234"));
*
- *
*/
final class ServerCookieEncoder extends CookieEncoder {
@@ -58,7 +54,7 @@ final class ServerCookieEncoder extends CookieEncoder {
* Lax instance that doesn't validate name and value, and that allows multiple
* cookies with the same name.
*/
- public static final ServerCookieEncoder LAX = new ServerCookieEncoder(false);
+ static final ServerCookieEncoder LAX = new ServerCookieEncoder(false);
private ServerCookieEncoder(boolean strict) {
super(strict);
@@ -67,12 +63,12 @@ private ServerCookieEncoder(boolean strict) {
/**
* Encodes the specified cookie name-value pair into a Set-Cookie header value.
*
- * @param name the cookie name
+ * @param name the cookie name
* @param value the cookie value
* @return a single Set-Cookie header value
*/
- public String encode(String name, String value) {
- return encode(new DefaultCookie(name, value));
+ String encode(String name, String value) {
+ return encode(new NettyCookie(name, value));
}
/**
@@ -81,7 +77,8 @@ public String encode(String name, String value) {
* @param cookie the cookie
* @return a single Set-Cookie header value
*/
- public String encode(Cookie cookie) {
+ String encode(NettyCookie cookie) {
+
final String name = requireNonNull(cookie, "cookie").name();
final String value = cookie.value() != null ? cookie.value() : "";
@@ -117,12 +114,16 @@ public String encode(Cookie cookie) {
add(buf, CookieHeaderNames.HTTPONLY);
}
+ if (cookie.sameSite() != null) {
+ add(buf, CookieHeaderNames.SAMESITE, cookie.sameSite());
+ }
return stripTrailingSeparator(buf);
}
- /** Deduplicate a list of encoded cookies by keeping only the last instance with a given name.
+ /**
+ * Deduplicate a list of encoded cookies by keeping only the last instance with a given name.
*
- * @param encoded The list of encoded cookies.
+ * @param encoded The list of encoded cookies.
* @param nameToLastIndex A map from cookie name to index of last cookie instance.
* @return The encoded list with all but the last instance of a named cookie.
*/
@@ -146,7 +147,7 @@ private static List deduplicate(List encoded, Map encode(Cookie... cookies) {
+ List encode(NettyCookie... cookies) {
if (requireNonNull(cookies, "cookies").length == 0) {
return Collections.emptyList();
}
@@ -155,7 +156,7 @@ public List encode(Cookie... cookies) {
Map nameToIndex = strict && cookies.length > 1 ? new HashMap<>() : null;
boolean hasDupdName = false;
for (int i = 0; i < cookies.length; i++) {
- Cookie c = cookies[i];
+ NettyCookie c = cookies[i];
encoded.add(encode(c));
if (nameToIndex != null) {
hasDupdName |= nameToIndex.put(c.name(), i) != null;
@@ -170,7 +171,7 @@ public List encode(Cookie... cookies) {
* @param cookies a bunch of cookies
* @return the corresponding bunch of Set-Cookie headers
*/
- public List encode(Collection extends Cookie> cookies) {
+ List encode(Collection extends NettyCookie> cookies) {
if (requireNonNull(cookies, "cookies").isEmpty()) {
return Collections.emptyList();
}
@@ -179,7 +180,7 @@ public List encode(Collection extends Cookie> cookies) {
Map nameToIndex = strict && cookies.size() > 1 ? new HashMap<>() : null;
int i = 0;
boolean hasDupdName = false;
- for (Cookie c : cookies) {
+ for (NettyCookie c : cookies) {
encoded.add(encode(c));
if (nameToIndex != null) {
hasDupdName |= nameToIndex.put(c.name(), i++) != null;
@@ -194,20 +195,20 @@ public List encode(Collection extends Cookie> cookies) {
* @param cookies a bunch of cookies
* @return the corresponding bunch of Set-Cookie headers
*/
- public List encode(Iterable extends Cookie> cookies) {
- Iterator extends Cookie> cookiesIt = requireNonNull(cookies, "cookies").iterator();
+ List encode(Iterable extends NettyCookie> cookies) {
+ Iterator extends NettyCookie> cookiesIt = requireNonNull(cookies, "cookies").iterator();
if (!cookiesIt.hasNext()) {
return Collections.emptyList();
}
List encoded = new ArrayList<>();
- Cookie firstCookie = cookiesIt.next();
+ NettyCookie firstCookie = cookiesIt.next();
Map nameToIndex = strict && cookiesIt.hasNext() ? new HashMap<>() : null;
int i = 0;
encoded.add(encode(firstCookie));
boolean hasDupdName = nameToIndex != null && nameToIndex.put(firstCookie.name(), i++) != null;
while (cookiesIt.hasNext()) {
- Cookie c = cookiesIt.next();
+ NettyCookie c = cookiesIt.next();
encoded.add(encode(c));
if (nameToIndex != null) {
hasDupdName |= nameToIndex.put(c.name(), i++) != null;
diff --git a/components/api/src/test/java/com/hotels/styx/api/ClientCookieDecoderTest.java b/components/api/src/test/java/com/hotels/styx/api/ClientCookieDecoderTest.java
new file mode 100644
index 0000000000..35d522c7fc
--- /dev/null
+++ b/components/api/src/test/java/com/hotels/styx/api/ClientCookieDecoderTest.java
@@ -0,0 +1,38 @@
+/*
+ Copyright (C) 2013-2020 Expedia Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package com.hotels.styx.api;
+
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ClientCookieDecoderTest {
+
+ @Test
+ void decodeCookieWithSameSite() {
+ String cookieValue = "hp_pos=\"\"; Domain=.dev-hotels.com; HttpOnly=true; Expires=Sun, 06 Nov 2040 08:49:37 GMT; SameSite=Strict";
+ NettyCookie cookie = ClientCookieDecoder.LAX.decode(cookieValue);
+
+ assertThat(cookie.sameSite(), equalTo("Strict"));
+ assertTrue(cookie.maxAge() != 0);
+ assertThat(cookie.domain(), equalTo(".dev-hotels.com"));
+ assertTrue(cookie.isHttpOnly());
+ }
+
+}
\ No newline at end of file
diff --git a/components/api/src/test/java/com/hotels/styx/api/HttpHeadersTest.java b/components/api/src/test/java/com/hotels/styx/api/HttpHeadersTest.java
index e17bb8b3a1..e4c5ab28f8 100644
--- a/components/api/src/test/java/com/hotels/styx/api/HttpHeadersTest.java
+++ b/components/api/src/test/java/com/hotels/styx/api/HttpHeadersTest.java
@@ -128,11 +128,26 @@ public void removesNullValues() {
@Test
public void doesNotAllowNullName() {
- assertThrows(NullPointerException.class, () -> new HttpHeaders.Builder()
+ assertThrows(IllegalArgumentException.class, () -> new HttpHeaders.Builder()
.add(null, "value")
.build());
}
+
+ @Test
+ public void doesNotAllowNewLines() {
+ assertThrows(IllegalArgumentException.class, () -> new HttpHeaders.Builder()
+ .add("key", "value\rvalue2")
+ .build());
+
+ assertThrows(IllegalArgumentException.class, () -> new HttpHeaders.Builder()
+ .add("key", "value\r\nvalue2")
+ .build());
+ assertThrows(IllegalArgumentException.class, () -> new HttpHeaders.Builder()
+ .add("key", "value\nvalue2")
+ .build());
+ }
+
@Test
public void setsDateHeaders() {
Instant time = ZonedDateTime.of(2015, 9, 10, 12, 2, 28, 0, UTC).toInstant();
diff --git a/components/api/src/test/java/com/hotels/styx/api/ResponseCookieTest.java b/components/api/src/test/java/com/hotels/styx/api/ResponseCookieTest.java
index 0df5272d1f..dad9d7c145 100644
--- a/components/api/src/test/java/com/hotels/styx/api/ResponseCookieTest.java
+++ b/components/api/src/test/java/com/hotels/styx/api/ResponseCookieTest.java
@@ -1,5 +1,5 @@
/*
- Copyright (C) 2013-2019 Expedia Inc.
+ Copyright (C) 2013-2020 Expedia Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,9 +15,13 @@
*/
package com.hotels.styx.api;
+
+import com.hotels.styx.api.ResponseCookie.SameSite;
import org.junit.jupiter.api.Test;
import static com.hotels.styx.api.ResponseCookie.responseCookie;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class ResponseCookieTest {
@@ -35,4 +39,14 @@ public void acceptsOnlyNonNullName() {
public void acceptsOnlyNonNullValue() {
assertThrows(NullPointerException.class, () -> responseCookie("name", null).build());
}
+
+
+ @Test
+ public void acceptSameSiteCookie() {
+ assertThat(
+ responseCookie("name", "value").sameSite(SameSite.Lax).build().sameSite().orElse(""),
+ equalTo(SameSite.Lax.name())
+ );
+ }
+
}
\ No newline at end of file
diff --git a/components/api/src/test/java/com/hotels/styx/api/ServerCookieEncoderTest.java b/components/api/src/test/java/com/hotels/styx/api/ServerCookieEncoderTest.java
index e293a8e7f4..aeba1b7bc2 100644
--- a/components/api/src/test/java/com/hotels/styx/api/ServerCookieEncoderTest.java
+++ b/components/api/src/test/java/com/hotels/styx/api/ServerCookieEncoderTest.java
@@ -1,5 +1,5 @@
/*
- Copyright (C) 2013-2019 Expedia Inc.
+ Copyright (C) 2013-2020 Expedia Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,20 +15,20 @@
*/
package com.hotels.styx.api;
-import io.netty.handler.codec.http.cookie.ClientCookieDecoder;
-import io.netty.handler.codec.http.cookie.Cookie;
+import com.hotels.styx.api.ResponseCookie.SameSite;
import org.junit.jupiter.api.Test;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.core.Is.is;
public class ServerCookieEncoderTest {
@Test
public void removesMaxAgeInFavourOfExpireIfDateIsInThePast() {
String cookieValue = "hp_pos=\"\"; Domain=.dev-hotels.com; Expires=Thu, 01-Jan-1970 00:00:10 GMT; Path=/";
- Cookie cookie = ClientCookieDecoder.LAX.decode(cookieValue);
+ NettyCookie cookie = ClientCookieDecoder.LAX.decode(cookieValue);
String encodedCookieValue = ServerCookieEncoder.LAX.encode(cookie);
assertThat(encodedCookieValue, not(containsString("Max-Age=0")));
@@ -38,9 +38,25 @@ public void removesMaxAgeInFavourOfExpireIfDateIsInThePast() {
@Test
public void willNotModifyMaxAgeIfPositive() {
String cookieValue = "hp_pos=\"\"; Domain=.dev-hotels.com; Max-Age=50; Expires=Thu, 01-Jan-1970 00:00:10 GMT; Path=/";
- Cookie cookie = ClientCookieDecoder.LAX.decode(cookieValue);
-
+ NettyCookie cookie = ClientCookieDecoder.LAX.decode(cookieValue);
+ assertThat(cookie.maxAge(), is(50L));
assertThat(ServerCookieEncoder.LAX.encode(cookie), containsString("Max-Age=50"));
+ }
+
+ @Test
+ public void setValidSameSite() {
+ NettyCookie cookie = new NettyCookie("key", "value");
+ cookie.setSameSite(SameSite.Lax.name());
+ cookie.setDomain(".domain.com");
+ cookie.setMaxAge(50);
+ assertThat(ServerCookieEncoder.LAX.encode(cookie), containsString("Lax"));
+ }
+
+ @Test
+ public void setAnySameSite() {
+ NettyCookie cookie = new NettyCookie("key", "value");
+ cookie.setSameSite("Test");
+ assertThat(ServerCookieEncoder.LAX.encode(cookie), containsString("SameSite=Test"));
}
}
\ No newline at end of file
diff --git a/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/HttpRequestOperation.java b/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/HttpRequestOperation.java
index fd44a39048..99a3f6405f 100644
--- a/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/HttpRequestOperation.java
+++ b/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/HttpRequestOperation.java
@@ -105,7 +105,7 @@ static DefaultHttpRequest toNettyRequest(LiveHttpRequest request) {
HttpVersion version = request.version();
HttpMethod method = request.method();
String url = request.url().toString();
- DefaultHttpRequest nettyRequest = new DefaultHttpRequest(toNettyVersion(version), toNettyMethod(method), url, false);
+ DefaultHttpRequest nettyRequest = new DefaultHttpRequest(toNettyVersion(version), toNettyMethod(method), url, true);
request.headers().forEach((name, value) ->
nettyRequest.headers().add(name, value));
diff --git a/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/NettyToStyxResponsePropagator.java b/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/NettyToStyxResponsePropagator.java
index ef5367387e..2718821ff4 100644
--- a/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/NettyToStyxResponsePropagator.java
+++ b/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/NettyToStyxResponsePropagator.java
@@ -61,6 +61,7 @@ final class NettyToStyxResponsePropagator extends SimpleChannelInboundHandler {
private static final Logger LOGGER = getLogger(NettyToStyxResponsePropagator.class);
private final AtomicBoolean responseCompleted = new AtomicBoolean(false);
+ private final AtomicBoolean responseReceived = new AtomicBoolean(false);
private final FluxSink sink;
private final LiveHttpRequest request;
@@ -119,11 +120,19 @@ protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Except
if (msg instanceof io.netty.handler.codec.http.HttpResponse) {
io.netty.handler.codec.http.HttpResponse nettyResponse = (io.netty.handler.codec.http.HttpResponse) msg;
+
+ if (!responseReceived.compareAndSet(false, true)) {
+ LOGGER.warn("Unexpected additional response received: " + nettyResponse);
+ ctx.channel().close();
+ return;
+ }
+
if (nettyResponse.getDecoderResult().isFailure()) {
emitResponseError(new BadHttpResponseException(origin, nettyResponse.getDecoderResult().cause()));
return;
}
+
ctx.channel().config().setAutoRead(false);
ctx.channel().read();
diff --git a/components/client/src/test/unit/java/com/hotels/styx/client/netty/connectionpool/NettyToStyxResponsePropagatorTest.java b/components/client/src/test/unit/java/com/hotels/styx/client/netty/connectionpool/NettyToStyxResponsePropagatorTest.java
index 9c04b5d9ff..fee64f5b93 100644
--- a/components/client/src/test/unit/java/com/hotels/styx/client/netty/connectionpool/NettyToStyxResponsePropagatorTest.java
+++ b/components/client/src/test/unit/java/com/hotels/styx/client/netty/connectionpool/NettyToStyxResponsePropagatorTest.java
@@ -1,5 +1,5 @@
/*
- Copyright (C) 2013-2019 Expedia Inc.
+ Copyright (C) 2013-2020 Expedia Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/components/proxy/src/main/kotlin/com/hotels/styx/services/YamlFileConfigurationService.kt b/components/proxy/src/main/kotlin/com/hotels/styx/services/YamlFileConfigurationService.kt
index 19f7fc54c6..530e035e8b 100644
--- a/components/proxy/src/main/kotlin/com/hotels/styx/services/YamlFileConfigurationService.kt
+++ b/components/proxy/src/main/kotlin/com/hotels/styx/services/YamlFileConfigurationService.kt
@@ -85,14 +85,14 @@ internal class YamlFileConfigurationService(
}
override fun start() = fileMonitoringService.start()
- .thenAccept {
+ .thenAcceptAsync {
LOGGER.info("service starting - {}", config.originsFile)
initialised.await()
LOGGER.info("service started - {}", config.originsFile)
}
override fun stop() = fileMonitoringService.stop()
- .thenAccept {
+ .thenAcceptAsync {
LOGGER.info("service stopped")
}
diff --git a/components/server/src/main/java/com/hotels/styx/server/netty/connectors/StyxToNettyResponseTranslator.java b/components/server/src/main/java/com/hotels/styx/server/netty/connectors/StyxToNettyResponseTranslator.java
index 02fb91a3fa..759be4364c 100644
--- a/components/server/src/main/java/com/hotels/styx/server/netty/connectors/StyxToNettyResponseTranslator.java
+++ b/components/server/src/main/java/com/hotels/styx/server/netty/connectors/StyxToNettyResponseTranslator.java
@@ -1,5 +1,5 @@
/*
- Copyright (C) 2013-2018 Expedia Inc.
+ Copyright (C) 2013-2019 Expedia Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -27,7 +27,7 @@ public HttpResponse toNettyResponse(LiveHttpResponse httpResponse) {
io.netty.handler.codec.http.HttpVersion version = toNettyVersion(httpResponse.version());
HttpResponseStatus httpResponseStatus = HttpResponseStatus.valueOf(httpResponse.status().code());
- DefaultHttpResponse nettyResponse = new DefaultHttpResponse(version, httpResponseStatus, false);
+ DefaultHttpResponse nettyResponse = new DefaultHttpResponse(version, httpResponseStatus, true);
httpResponse.headers().forEach(httpHeader ->
nettyResponse.headers().add(httpHeader.name(), httpHeader.value()));
diff --git a/components/server/src/test/java/com/hotels/styx/server/netty/connectors/StyxToNettyResponseTranslatorTest.java b/components/server/src/test/java/com/hotels/styx/server/netty/connectors/StyxToNettyResponseTranslatorTest.java
index c1cfc3ae30..925ffca131 100644
--- a/components/server/src/test/java/com/hotels/styx/server/netty/connectors/StyxToNettyResponseTranslatorTest.java
+++ b/components/server/src/test/java/com/hotels/styx/server/netty/connectors/StyxToNettyResponseTranslatorTest.java
@@ -1,5 +1,5 @@
/*
- Copyright (C) 2013-2019 Expedia Inc.
+ Copyright (C) 2013-2020 Expedia Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/pom.xml b/pom.xml
index e5c2a1911a..cf0a3ad6e1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1005,6 +1005,7 @@
default.properties
**/GraphiteReporter.java
**/ServerCookieEncoder.java
+ **/ClientCookieDecoder.java
**/MultithreadedStressTester.java
**/RelationshipTester.java
**/EqualsTester.java
diff --git a/system-tests/e2e-suite/src/test/scala/com/hotels/styx/proxy/BadResponseFromOriginSpec.scala b/system-tests/e2e-suite/src/test/scala/com/hotels/styx/proxy/BadResponseFromOriginSpec.scala
index 22624a1323..915c921082 100644
--- a/system-tests/e2e-suite/src/test/scala/com/hotels/styx/proxy/BadResponseFromOriginSpec.scala
+++ b/system-tests/e2e-suite/src/test/scala/com/hotels/styx/proxy/BadResponseFromOriginSpec.scala
@@ -29,7 +29,7 @@ import com.hotels.styx.support.{NettyOrigins, TestClientSupport}
import com.hotels.styx.{DefaultStyxConfiguration, StyxProxySpec}
import io.netty.buffer.Unpooled
import io.netty.channel.ChannelHandlerContext
-import io.netty.handler.codec.http.HttpHeaders.Names.{CONTENT_LENGTH, HOST, TRANSFER_ENCODING}
+import io.netty.handler.codec.http.HttpHeaders.Names._
import io.netty.handler.codec.http.HttpHeaders.Values.CHUNKED
import io.netty.handler.codec.http.HttpMethod._
import io.netty.handler.codec.http.HttpResponseStatus.OK
@@ -110,6 +110,30 @@ class BadResponseFromOriginSpec extends FunSpec
styxServer.metricsSnapshot.count("styx.response.status.502").get should be(1)
}
}
+
+
+ it("ignores second response from an origin") {
+ val twoResponses = (ctx: ChannelHandlerContext, msg: scala.Any) => {
+ val response = new DefaultFullHttpResponse(HTTP_1_1, OK)
+ response.headers().add(CONTENT_LENGTH, 0)
+ response.headers().add(ACCEPT_ENCODING, "ENCODING")
+ ctx.write(response)
+
+ val response2 = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.NOT_FOUND)
+ response2.headers().add(ACCEPT_CHARSET, "CHARSET")
+ ctx.writeAndFlush(response2)
+ }
+
+ originRespondingWith(twoResponses)
+ val req = get("/badResponseFromOriginSpec/4")
+ .addHeader(HOST, styxServer.proxyHost)
+ .build()
+
+ val resp = decodedRequest(req)
+ assert(resp.status().code() == 200)
+ assert(resp.header(ACCEPT_ENCODING).get() == "ENCODING")
+ assert(!resp.header(ACCEPT_CHARSET).isPresent)
+ }
}
def response200OkWithoutWaitingForFullRequest(messageBody: String): (ChannelHandlerContext, Any) => Unit = {
diff --git a/system-tests/e2e-suite/src/test/scala/com/hotels/styx/proxy/HeadersSpec.scala b/system-tests/e2e-suite/src/test/scala/com/hotels/styx/proxy/HeadersSpec.scala
index fd346e6293..fe279ece4e 100644
--- a/system-tests/e2e-suite/src/test/scala/com/hotels/styx/proxy/HeadersSpec.scala
+++ b/system-tests/e2e-suite/src/test/scala/com/hotels/styx/proxy/HeadersSpec.scala
@@ -1,5 +1,5 @@
/*
- Copyright (C) 2013-2018 Expedia Inc.
+ Copyright (C) 2013-2019 Expedia Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -373,6 +373,41 @@ class HeadersSpec extends FunSpec
assert(!resp.contentLength.isPresent, "content length header should be empty")
assert(resp.header(TRANSFER_ENCODING).get() == CHUNKED)
}
+
+ it("should support new lines from received request headers in folding format (no CR)") {
+ recordingBackend.stub(urlPathEqualTo("/headers"), aResponse.withStatus(200))
+
+ val req = get("/headers")
+ .addHeader(HOST, styxServer.proxyHost)
+ .addHeader(ACCEPT_ENCODING, "UTF\n 8")
+ .build()
+
+ assert(req.header(ACCEPT_ENCODING).get() == "UTF\n 8")
+
+ decodedRequest(req)
+
+ recordingBackend.verify(
+ getRequestedFor(urlPathEqualTo("/headers"))
+ .withHeader(ACCEPT_ENCODING, equalTo("UTF 8"))
+ )
+ }
+ it("should support new lines in received request headers in obsolete folding format (CRLF)") {
+ recordingBackend.stub(urlPathEqualTo("/headers"), aResponse.withStatus(200))
+
+ val req = get("/headers")
+ .addHeader(HOST, styxServer.proxyHost)
+ .addHeader(ACCEPT_ENCODING, "UTF\r\n 8")
+ .build()
+
+ assert(req.header(ACCEPT_ENCODING).get() == "UTF\r\n 8")
+
+ decodedRequest(req)
+
+ recordingBackend.verify(
+ getRequestedFor(urlPathEqualTo("/headers"))
+ .withHeader(ACCEPT_ENCODING, equalTo("UTF 8"))
+ )
+ }
}
describe("handling cookies") {
diff --git a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/proxy/CookieSpec.kt b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/proxy/CookieSpec.kt
new file mode 100644
index 0000000000..e3f57c41b1
--- /dev/null
+++ b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/proxy/CookieSpec.kt
@@ -0,0 +1,78 @@
+/*
+ Copyright (C) 2013-2020 Expedia Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package com.hotels.styx.proxy
+
+import com.hotels.styx.api.HttpHeaderNames
+import com.hotels.styx.api.HttpRequest.get
+import com.hotels.styx.client.StyxHttpClient
+import com.hotels.styx.support.StyxServerProvider
+import com.hotels.styx.support.proxyHttpHostHeader
+import com.hotels.styx.support.wait
+import io.kotlintest.Spec
+import io.kotlintest.shouldBe
+import io.kotlintest.specs.StringSpec
+
+class CookieSpec : StringSpec() {
+
+ init {
+ "Response Cookie should contain the SameSite attribute " {
+
+ client.send(
+ get("/")
+ .header(HttpHeaderNames.HOST, styxServer().proxyHttpHostHeader())
+ .build())
+ .wait()!!
+ .let {
+ it.cookies().iterator().next().sameSite().get() shouldBe "Strict"
+ }
+ }
+ }
+
+
+ val client: StyxHttpClient = StyxHttpClient.Builder().build()
+
+ val styxServer = StyxServerProvider("""
+ proxy:
+ connectors:
+ http:
+ port: 0
+ maxInitialLength: 100
+
+ admin:
+ connectors:
+ http:
+ port: 0
+
+ httpPipeline:
+ type: StaticResponseHandler
+ config:
+ status: 200
+ content: "Test origin."
+ headers:
+ - name: Set-Cookie
+ value: name=value; Secure=true; HttpOnly=true; SameSite=Strict
+ """.trimIndent())
+
+
+ override fun beforeSpec(spec: Spec) {
+ styxServer.restart()
+ }
+
+ override fun afterSpec(spec: Spec) {
+ styxServer.stop()
+ }
+
+}
\ No newline at end of file