CookiePathFilter.java
/*
* GovWay - A customizable API Gateway
* https://govway.org
*
* Copyright (c) 2005-2026 Link.it srl (https://link.it).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3, as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.openspcoop2.utils.transport.http;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.openspcoop2.utils.LoggerWrapperFactory;
import org.slf4j.Logger;
/**
* CookiePathFilter
*
* Filtro che normalizza il path dei cookie al context path dell'applicazione.
* Utile quando si usa STRICT_SERVLET_COMPLIANCE=true su Tomcat con applicazioni
* che usano Servlet spec 2.5, dove i cookie potrebbero avere path più restrittivi.
*
* Configurazione init-param:
* - cookiePath.enabled: true/false (default: true)
* - cookiePath.logCategory: categoria di log (default: classe del filtro)
*
* @author Andrea Poli (apoli@link.it)
* @author $Author$
* @version $Rev$, $Date$
*/
public class CookiePathFilter implements Filter {
private boolean enabled = true;
private boolean debug = false;
private Logger log;
private static final String CONFIG_ENABLED = "cookiePath.enabled";
private static final String CONFIG_LOG = "cookiePath.logCategory";
private static final String CONFIG_DEBUG = "cookiePath.debug";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
if(filterConfig != null) {
String tmp = filterConfig.getInitParameter(CONFIG_ENABLED);
if(tmp != null && "false".equalsIgnoreCase(tmp.trim())) {
this.enabled = false;
}
tmp = filterConfig.getInitParameter(CONFIG_LOG);
if(tmp != null) {
this.log = LoggerWrapperFactory.getLogger(tmp.trim());
} else {
this.log = LoggerWrapperFactory.getLogger(CookiePathFilter.class);
}
tmp = filterConfig.getInitParameter(CONFIG_DEBUG);
if(tmp != null && "true".equalsIgnoreCase(tmp.trim())) {
this.debug = true;
}
}
}
private void log(String msg) {
if(this.debug && this.log!=null) {
this.log.debug(msg);
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!this.enabled) {
chain.doFilter(request, response);
return;
}
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String contextPath = httpRequest.getContextPath();
if(contextPath == null || contextPath.isEmpty()) {
contextPath = "/";
}
log("[CookiePathFilter] doFilter - URI: " + httpRequest.getRequestURI() + ", contextPath: " + contextPath);
chain.doFilter(request, new CookiePathResponseWrapper(httpResponse, contextPath, this.log, this.debug));
}
@Override
public void destroy() {
// nop
}
}
class CookiePathResponseWrapper extends HttpServletResponseWrapper {
private static final String PATH_ATTR = "Path=";
private static final Pattern PATH_PATTERN = Pattern.compile("(;\\s*Path=)([^;\\s]*)", Pattern.CASE_INSENSITIVE);
private final HttpServletResponse originalResponse;
private final String contextPath;
private final Logger log;
private final boolean debug;
private boolean cookiesProcessed = false;
public CookiePathResponseWrapper(HttpServletResponse response, String contextPath, Logger log, boolean debug) {
super(response);
this.originalResponse = response;
this.contextPath = contextPath;
this.log = log;
this.debug = debug;
}
void log(String msg) {
if(this.debug && this.log!=null) {
this.log.debug(msg);
}
}
@Override
public void addCookie(Cookie cookie) {
log("[CookiePathFilter] addCookie chiamato - cookie: " + (cookie != null ? cookie.getName() : "null"));
if (cookie != null) {
String currentPath = cookie.getPath();
log("[CookiePathFilter] addCookie - nome: " + cookie.getName() + ", path originale: '" + currentPath + "', contextPath: '" + this.contextPath + "'");
// Se il path non è impostato o è più restrittivo del context path, lo normalizziamo
if (currentPath == null || currentPath.isEmpty()) {
cookie.setPath(this.contextPath);
log("[CookiePathFilter] addCookie - path impostato a: '" + this.contextPath + "'");
} else if (currentPath.startsWith(this.contextPath) && currentPath.length() > this.contextPath.length()) {
log("[CookiePathFilter] addCookie - path normalizzato da '" + currentPath + "' a '" + this.contextPath + "'");
cookie.setPath(this.contextPath);
} else {
log("[CookiePathFilter] addCookie - path non modificato: '" + currentPath + "'");
}
}
super.addCookie(cookie);
}
@Override
public void setHeader(String name, String value) {
if (HttpConstants.SET_COOKIE.equalsIgnoreCase(name) && value != null) {
String originalValue = value;
value = normalizeCookiePath(value);
String msg = "[CookiePathFilter] setHeader Set-Cookie - originale: '" + originalValue + "' -> normalizzato : '" + value + "'";
log(msg);
}
super.setHeader(name, value);
}
@Override
public void addHeader(String name, String value) {
if (HttpConstants.SET_COOKIE.equalsIgnoreCase(name) && value != null) {
String originalValue = value;
value = normalizeCookiePath(value);
log("[CookiePathFilter] addHeader Set-Cookie - originale: '" + originalValue + "' -> normalizzato: '" + value + "'");
}
super.addHeader(name, value);
}
/**
* Processa e riscrive i cookie Set-Cookie prima del commit della risposta
*/
private synchronized void processSetCookieHeaders() {
if (this.cookiesProcessed) {
return;
}
this.cookiesProcessed = true;
try {
Collection<String> cookieHeaders = this.originalResponse.getHeaders(HttpConstants.SET_COOKIE);
if (cookieHeaders == null || cookieHeaders.isEmpty()) {
log("[CookiePathFilter] processSetCookieHeaders - nessun Set-Cookie trovato");
return;
}
log("[CookiePathFilter] processSetCookieHeaders - trovati " + cookieHeaders.size() + " header Set-Cookie");
boolean first = true;
for (String cookieHeader : cookieHeaders) {
if (cookieHeader == null || cookieHeader.isEmpty()) {
continue;
}
String normalizedHeader = normalizeCookiePath(cookieHeader);
log("[CookiePathFilter] processSetCookieHeaders - originale: '" + cookieHeader + "' -> normalizzato: '" + normalizedHeader + "'");
if (first) {
// Il primo setHeader sovrascrive tutti gli header Set-Cookie esistenti
this.originalResponse.setHeader(HttpConstants.SET_COOKIE, normalizedHeader);
first = false;
} else {
this.originalResponse.addHeader(HttpConstants.SET_COOKIE, normalizedHeader);
}
}
} catch (Exception e) {
log("[CookiePathFilter] processSetCookieHeaders - errore: " + e.getMessage());
/**e.printStackTrace();*/
}
}
@Override
public void flushBuffer() throws IOException {
log("[CookiePathFilter] flushBuffer chiamato");
processSetCookieHeaders();
super.flushBuffer();
}
@Override
public void sendError(int sc) throws IOException {
String msg = "[CookiePathFilter] sendError(" + sc + ") chiamato ";
log(msg);
processSetCookieHeaders();
super.sendError(sc);
}
@Override
public void sendError(int sc, String msg) throws IOException {
log("[CookiePathFilter] sendError(" + sc + ", " + msg + ") chiamato");
processSetCookieHeaders();
super.sendError(sc, msg);
}
@Override
public void sendRedirect(String location) throws IOException {
log("[CookiePathFilter] sendRedirect(" + location + ") chiamato");
processSetCookieHeaders();
super.sendRedirect(location);
}
@Override
public PrintWriter getWriter() throws IOException {
log("[CookiePathFilter] getWriter chiamato");
// Wrappa il writer per processare i cookie alla prima scrittura
return new CookiePathPrintWriter(super.getWriter(), this);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
log("[CookiePathFilter] getOutputStream chiamato");
// Wrappa l'output stream per processare i cookie alla prima scrittura
return new CookiePathServletOutputStream(super.getOutputStream(), this);
}
void triggerCookieProcessing() {
processSetCookieHeaders();
}
private String normalizeCookiePath(String cookieHeader) {
Matcher matcher = PATH_PATTERN.matcher(cookieHeader);
if (matcher.find()) {
String currentPath = matcher.group(2);
// Rimuovi virgolette se presenti (Tomcat con STRICT_SERVLET_COMPLIANCE le aggiunge)
String unquotedPath = unquote(currentPath);
log("[CookiePathFilter] normalizeCookiePath - path: '" + currentPath + "' -> unquoted: '" + unquotedPath + "'");
// Se il path ha virgolette o è più restrittivo del context path, lo normalizziamo
boolean hasQuotes = currentPath!=null && !currentPath.equals(unquotedPath);
boolean isMoreRestrictive = unquotedPath!=null && unquotedPath.startsWith(this.contextPath) && unquotedPath.length() > this.contextPath.length();
if (hasQuotes || isMoreRestrictive) {
// Sostituiamo sempre con il context path SENZA virgolette
String newHeader = matcher.replaceFirst("$1" + Matcher.quoteReplacement(this.contextPath));
log("[CookiePathFilter] normalizeCookiePath - PATH NORMALIZZATO da '" + currentPath + "' a '" + this.contextPath + "' (hasQuotes=" + hasQuotes + ", isMoreRestrictive=" + isMoreRestrictive + ")");
return newHeader;
}
} else {
// Nessun Path presente, aggiungiamo il context path
String newHeader = cookieHeader + "; " + PATH_ATTR + this.contextPath;
log("[CookiePathFilter] normalizeCookiePath - NESSUN PATH trovato, aggiunto: '" + this.contextPath + "'");
return newHeader;
}
return cookieHeader;
}
private String unquote(String value) {
if (value != null && value.length() >= 2 && value.startsWith("\"") && value.endsWith("\"")) {
return value.substring(1, value.length() - 1);
}
return value;
}
}
class CookiePathServletOutputStream extends ServletOutputStream {
private final ServletOutputStream wrapped;
private final CookiePathResponseWrapper responseWrapper;
private boolean firstWrite = true;
public CookiePathServletOutputStream(ServletOutputStream wrapped, CookiePathResponseWrapper responseWrapper) {
this.wrapped = wrapped;
this.responseWrapper = responseWrapper;
}
private void checkFirstWrite() {
if (this.firstWrite) {
this.firstWrite = false;
this.responseWrapper.log("[CookiePathFilter] OutputStream - prima scrittura, processo cookie");
this.responseWrapper.triggerCookieProcessing();
}
}
@Override
public void write(int b) throws IOException {
checkFirstWrite();
this.wrapped.write(b);
}
@Override
public void write(byte[] b) throws IOException {
checkFirstWrite();
this.wrapped.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
checkFirstWrite();
this.wrapped.write(b, off, len);
}
@Override
public void flush() throws IOException {
checkFirstWrite();
this.wrapped.flush();
}
@Override
public void close() throws IOException {
checkFirstWrite();
this.wrapped.close();
}
@Override
public boolean isReady() {
return this.wrapped.isReady();
}
@Override
public void setWriteListener(javax.servlet.WriteListener writeListener) {
this.wrapped.setWriteListener(writeListener);
}
}
class CookiePathPrintWriter extends PrintWriter {
private final CookiePathResponseWrapper responseWrapper;
private boolean firstWrite = true;
public CookiePathPrintWriter(PrintWriter wrapped, CookiePathResponseWrapper responseWrapper) {
super(wrapped);
this.responseWrapper = responseWrapper;
}
private void checkFirstWrite() {
if (this.firstWrite) {
this.firstWrite = false;
this.responseWrapper.log("[CookiePathFilter] PrintWriter - prima scrittura, processo cookie");
this.responseWrapper.triggerCookieProcessing();
}
}
@Override
public void write(int c) {
checkFirstWrite();
super.write(c);
}
@Override
public void write(char[] buf, int off, int len) {
checkFirstWrite();
super.write(buf, off, len);
}
@Override
public void write(String s, int off, int len) {
checkFirstWrite();
super.write(s, off, len);
}
@Override
public void flush() {
checkFirstWrite();
super.flush();
}
}