Skip to content

Commit

Permalink
[UNDERTOW-1489] Keep track of duplicate cookie entries at HttpServlet…
Browse files Browse the repository at this point in the history
…ResponseImpl, and add them to header via Connectors.addCookie

This solution is an alternative to keeping track of those entries inside the Exchange. The response cookies map in that class is part of public spi and would break compatibility.
  • Loading branch information
fl4via committed Jun 17, 2019
1 parent c293f10 commit 98f00d5
Show file tree
Hide file tree
Showing 7 changed files with 426 additions and 2 deletions.
12 changes: 12 additions & 0 deletions core/src/main/java/io/undertow/server/Connectors.java
Expand Up @@ -101,6 +101,18 @@ public static void flattenCookies(final HttpServerExchange exchange) {
} }
} }


/**
* Adds the cookie into the response header map. This should be called
* before the response is started.
*
* @param exchange The server exchange
* @param cookie The cookie
*/
public static void addCookie(final HttpServerExchange exchange, Cookie cookie) {
boolean enableRfc6265Validation = exchange.getConnection().getUndertowOptions().get(UndertowOptions.ENABLE_RFC6265_COOKIE_VALIDATION, UndertowOptions.DEFAULT_ENABLE_RFC6265_COOKIE_VALIDATION);
exchange.getResponseHeaders().add(Headers.SET_COOKIE, getCookieString(cookie, enableRfc6265Validation));
}

/** /**
* Attached buffered data to the exchange. The will generally be used to allow data to be re-read. * Attached buffered data to the exchange. The will generally be used to allow data to be re-read.
* *
Expand Down
Expand Up @@ -32,17 +32,20 @@
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.TreeMap;
import java.util.function.Supplier; import java.util.function.Supplier;


import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream; import javax.servlet.ServletOutputStream;
import javax.servlet.SessionTrackingMode; import javax.servlet.SessionTrackingMode;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSession;


import io.undertow.UndertowLogger; import io.undertow.UndertowLogger;
import io.undertow.server.Connectors;
import io.undertow.server.HttpServerExchange; import io.undertow.server.HttpServerExchange;
import io.undertow.server.ResponseCommitListener;
import io.undertow.server.handlers.Cookie;
import io.undertow.server.protocol.http.HttpAttachments; import io.undertow.server.protocol.http.HttpAttachments;
import io.undertow.servlet.UndertowServletMessages; import io.undertow.servlet.UndertowServletMessages;
import io.undertow.servlet.handlers.ServletRequestContext; import io.undertow.servlet.handlers.ServletRequestContext;
Expand Down Expand Up @@ -85,6 +88,7 @@ public final class HttpServletResponseImpl implements HttpServletResponse {
private String contentType; private String contentType;
private String charset; private String charset;
private Supplier<Map<String, String>> trailerSupplier; private Supplier<Map<String, String>> trailerSupplier;
private Map<String, Map<String, Cookie>> duplicateCookies;


public HttpServletResponseImpl(final HttpServerExchange exchange, final ServletContextImpl servletContext) { public HttpServletResponseImpl(final HttpServerExchange exchange, final ServletContextImpl servletContext) {
this.exchange = exchange; this.exchange = exchange;
Expand All @@ -97,14 +101,41 @@ public HttpServerExchange getExchange() {
} }


@Override @Override
public void addCookie(final Cookie cookie) { public void addCookie(final javax.servlet.http.Cookie cookie) {
if (insideInclude) { if (insideInclude) {
return; return;
} }
final ServletCookieAdaptor servletCookieAdaptor = new ServletCookieAdaptor(cookie); final ServletCookieAdaptor servletCookieAdaptor = new ServletCookieAdaptor(cookie);
if (cookie.getVersion() == 0) { if (cookie.getVersion() == 0) {
servletCookieAdaptor.setVersion(servletContext.getDeployment().getDeploymentInfo().getDefaultCookieVersion()); servletCookieAdaptor.setVersion(servletContext.getDeployment().getDeploymentInfo().getDefaultCookieVersion());
} }
// test for duplicate entry
if (exchange.getResponseCookies().containsKey(servletCookieAdaptor.getName())) {
final String cookieName = servletCookieAdaptor.getName();
final String path = servletCookieAdaptor.getPath();
final Cookie otherCookie = exchange.getResponseCookies().get(cookieName);
final String otherCookiePath = otherCookie.getPath();
// if both cookies have same path and name, overwrite previous cookie
if ((path == otherCookiePath) || (path != null && path.equals(otherCookiePath))) {
exchange.setResponseCookie(servletCookieAdaptor);
}
// else, create a duplicate cookie entry
else {
final Map<String, Cookie> cookiesByPath;
if (duplicateCookies == null) {
duplicateCookies = new TreeMap<>();
exchange.addResponseCommitListener(
new DuplicateCookieCommitListener());
}
if (duplicateCookies.containsKey(cookieName)) {
cookiesByPath = duplicateCookies.get(cookieName);
} else {
cookiesByPath = new TreeMap<>();
duplicateCookies.put(cookieName, cookiesByPath);
}
cookiesByPath.put(otherCookiePath == null ? "null" : otherCookiePath, otherCookie);
}
}
exchange.setResponseCookie(servletCookieAdaptor); exchange.setResponseCookie(servletCookieAdaptor);
} }


Expand Down Expand Up @@ -815,4 +846,17 @@ public void setTrailerFields(Supplier<Map<String, String>> supplier) {
public Supplier<Map<String, String>> getTrailerFields() { public Supplier<Map<String, String>> getTrailerFields() {
return trailerSupplier; return trailerSupplier;
} }

private class DuplicateCookieCommitListener implements
ResponseCommitListener {

@Override
public void beforeCommit(HttpServerExchange exchange) {
for (Map.Entry<String, Map<String, Cookie>> duplicateCookiesEntry : duplicateCookies.entrySet()) {
for (Map.Entry<String, Cookie> cookiesByPathEntry : duplicateCookiesEntry.getValue().entrySet()) {
Connectors.addCookie(exchange, cookiesByPathEntry.getValue());
}
}
}
}
} }
@@ -0,0 +1,48 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2019 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 io.undertow.servlet.test.response.cookies;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* Simple servlet that adds cookies to response.
*
* @author Flavia Rainone
*/
public class AddCookiesServlet extends HttpServlet {

@Override
protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
Cookie cookie1 = new Cookie("test1", "test1");
cookie1.setPath("/test");

Cookie cookie2 = new Cookie("test2", "test2");

resp.addCookie(cookie1);
resp.addCookie(cookie2);

resp.getWriter().append("Served at: ").append(req.getContextPath());
}
}
@@ -0,0 +1,49 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2019 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 io.undertow.servlet.test.response.cookies;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* Servlet that adds duplicate cookies (i.e., with the same name) to response.
*
* @author Flavia Rainone
*/
public class DuplicateCookiesServlet extends HttpServlet {

@Override
protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
Cookie cookie1 = new Cookie("test", "test");
cookie1.setPath("/test");

Cookie cookie2 = new Cookie("test", "test");
cookie2.setPath("/test2");

resp.addCookie(cookie1);
resp.addCookie(cookie2);

resp.getWriter().append("Served at: ").append(req.getContextPath());
}
}
@@ -0,0 +1,64 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2019 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 io.undertow.servlet.test.response.cookies;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* Servlet that emulates a buggy behavior where JSessionID cookie is added several times with
* wrong path, and a few of them with max age limits for cookie expiration.
*
* @author Flavia Rainone
*/
public class JSessionIDCookiesServlet extends HttpServlet {

@Override
protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
javax.servlet.http.Cookie cookie1 = new javax.servlet.http.Cookie("JSESSIONID", "_bug_fix");
cookie1.setPath("/path1");
cookie1.setMaxAge(0);

javax.servlet.http.Cookie cookie2 = new javax.servlet.http.Cookie("JSESSIONID", "_bug_fix");
cookie2.setPath("/path2");
cookie2.setMaxAge(0);

javax.servlet.http.Cookie cookie3 = new javax.servlet.http.Cookie("JSESSIONID", "_bug_fix");
cookie1.setPath("/path3");
cookie1.setMaxAge(500);

javax.servlet.http.Cookie cookie4 = new javax.servlet.http.Cookie("JSESSIONID", "_bug_fix");
cookie2.setPath("/path4");
cookie2.setMaxAge(1000);

resp.addCookie(cookie1);
resp.addCookie(cookie2);
resp.addCookie(cookie3);
resp.addCookie(cookie4);

// creating session -> additional set-cookie
req.getSession().setAttribute("CleanSessions", true);

resp.getWriter().append("Served at: ").append(req.getContextPath());
}
}
@@ -0,0 +1,61 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2019 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 io.undertow.servlet.test.response.cookies;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* Servlet that adds multiple cookies with same name and a few of which sharing
* the same path, to test cookies with same path and name being correctly
* overriden.
*
* @author Flavia Rainone
*/
public class OverwriteCookiesServlet extends HttpServlet {

@Override
protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
Cookie cookie1 = new javax.servlet.http.Cookie("test", "test1");
cookie1.setPath("/test");

Cookie cookie2 = new javax.servlet.http.Cookie("test", "test2");
cookie2.setPath("/test");

Cookie cookie3 = new javax.servlet.http.Cookie("test", "test3");
Cookie cookie4 = new javax.servlet.http.Cookie("test", "test4");
Cookie cookie5 = new javax.servlet.http.Cookie("test", "test5");

resp.addCookie(cookie1);
resp.addCookie(cookie2);
resp.addCookie(cookie3);
resp.addCookie(cookie4);
resp.addCookie(cookie5);

// creating session -> additional jsessionid set-cookie
req.getSession().setAttribute("CleanSessions", true);

resp.getWriter().append("Served at: ").append(req.getContextPath());
}
}

0 comments on commit 98f00d5

Please sign in to comment.