CookieSameSiteFilter.java

/*
 * GovWay - A customizable API Gateway 
 * https://govway.org
 * 
 * Copyright (c) 2005-2025 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.net.HttpCookie;
import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.openspcoop2.utils.LoggerWrapperFactory;
import org.openspcoop2.utils.transport.TransportUtils;
import org.slf4j.Logger;

import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponseWrapper;

/**
* CookieSameSiteFilter
*
* @author Andrea Poli (apoli@link.it)
* @author $Author$
* @version $Rev$, $Date$
*/
public class CookieSameSiteFilter implements jakarta.servlet.Filter {

	private boolean enabled = false;
	private String strictValue = "Strict";
	private Logger log;
	
	private static final String SAME_SITE_CONFIG_ENABLED = "sameSite.enabled";
	private static final String SAME_SITE_CONFIG_VAUE = "sameSite.value";
	private static final String SAME_SITE_CONFIG_LOG = "sameSite.logCategory";
	
	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		if(filterConfig!=null) {
			String tmp = filterConfig.getInitParameter(SAME_SITE_CONFIG_ENABLED);
			if(tmp!=null && "true".equalsIgnoreCase(tmp.trim())) {
				this.enabled = true;
			}
			
			tmp = filterConfig.getInitParameter(SAME_SITE_CONFIG_VAUE);
			if(tmp!=null) {
				this.strictValue = tmp.trim();
			}
			
			tmp = filterConfig.getInitParameter(SAME_SITE_CONFIG_LOG);
			if(tmp!=null) {
				this.log = LoggerWrapperFactory.getLogger(tmp.trim());
			}
			else {
				this.log = LoggerWrapperFactory.getLogger(CookieSameSiteFilter.class);
			}
		}
	}
	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

	    if (!this.enabled) {
	    	chain.doFilter(request, response);
	    	return;
	    }

		chain.doFilter(request, new CookieSameSiteResponseProxy((HttpServletResponse)response, this.log, this.strictValue));
		
	}
	
}

class CookieSameSiteResponseProxy extends HttpServletResponseWrapper{

	private final HttpServletResponse response;
	private Logger log;
	private String strictValue;

	public CookieSameSiteResponseProxy(final HttpServletResponse resp, Logger log, String strictValue) {
		super(resp);
		this.response = resp;
		this.log = log;
		this.strictValue = strictValue;
	}

	@Override
	public void sendError(final int sc) throws IOException {
		processSameSiteCookie();
		super.sendError(sc);
	}
	@Override
	public void sendError(final int sc, final String msg) throws IOException {
		processSameSiteCookie();
		super.sendError(sc, msg);
	}
	
	@Override
	public void sendRedirect(final String location) throws IOException {
		processSameSiteCookie();
		super.sendRedirect(location);
	}


	@Override
	public PrintWriter getWriter() throws IOException {
		processSameSiteCookie();
		return super.getWriter();
	}
	@Override
	public ServletOutputStream getOutputStream() throws IOException {
		processSameSiteCookie();
		return super.getOutputStream();
	}

	private void processSameSiteCookie() {

		List<String> headers = TransportUtils.getHeaderValues(this.response, HttpConstants.SET_COOKIE);
		if(headers==null || headers.isEmpty()) {
			return;
		}
		
		boolean firstHeader = true;
		for (final String hdr : headers) {

			if(hdr==null || StringUtils.isEmpty(hdr.trim())) {
				continue;
			}
			
			try {
				//this parser only parses name and value, we only need the name.
				List<HttpCookie> parsedCookies = HttpCookie.parse(hdr);

				if (parsedCookies != null && !parsedCookies.isEmpty()) {
					set(hdr, firstHeader);
				}
	
				firstHeader=false;
				
			} catch(Exception e) {
				// Should not get here
				this.log.trace("Process cookie header '"+hdr+"' failed: "+e.getMessage(), e);
			}

		}
	}
	private void set(String hdr, boolean firstHeader) {
		String newValue = hdr;
		if (!hdr.contains(HttpConstants.SET_COOKIE_SAME_SITE_PARAMETER)) {
			newValue = String.format("%s; %s", hdr,
					HttpConstants.SET_COOKIE_SAME_SITE_PARAMETER + "=" + this.strictValue);                
		} 
		
		if (firstHeader) {                
			this.response.setHeader(HttpConstants.SET_COOKIE, newValue);
		} else {
			this.response.addHeader(HttpConstants.SET_COOKIE, newValue);
		}
	}

}