From 27e4d2628278eea18976cab9b632d2ae80f9859c Mon Sep 17 00:00:00 2001 From: Florian Necas Date: Mon, 16 Mar 2026 20:56:49 +0100 Subject: [PATCH 1/2] feat: add request header white and black lists --- pom.xml | 2 + .../it/geosolutions/httpproxy/HTTPProxy.java | 29 +++ .../geosolutions/httpproxy/ProxyConfig.java | 64 +++++ .../java/it/geosolutions/httpproxy/Utils.java | 17 ++ src/main/resources/proxy.properties | 8 + .../httpproxy/RequestHeaderFilterTest.java | 220 ++++++++++++++++++ src/test/resources/proxy.properties | 3 + 7 files changed, 343 insertions(+) create mode 100644 src/test/java/it/geosolutions/httpproxy/RequestHeaderFilterTest.java diff --git a/pom.xml b/pom.xml index 955d423..5d2de9f 100644 --- a/pom.xml +++ b/pom.xml @@ -157,6 +157,8 @@ UTF-8 2.19.0 4.5.13 + 8 + 8 diff --git a/src/main/java/it/geosolutions/httpproxy/HTTPProxy.java b/src/main/java/it/geosolutions/httpproxy/HTTPProxy.java index 1a35098..e1a56a5 100644 --- a/src/main/java/it/geosolutions/httpproxy/HTTPProxy.java +++ b/src/main/java/it/geosolutions/httpproxy/HTTPProxy.java @@ -847,12 +847,41 @@ private ProxyInfo setProxyRequestHeaders(URL url, HttpServletRequest httpServlet Enumeration enumerationOfHeaderNames = httpServletRequest.getHeaderNames(); + // //////////////////////////////////////// + // Load header whitelist/blacklist for + // filtering forwarded request headers. + // //////////////////////////////////////// + + Set headerWhitelist = proxyConfig.getRequestHeaderWhitelist(); + Set headerBlacklist = proxyConfig.getRequestHeaderBlacklist(); + while (enumerationOfHeaderNames.hasMoreElements()) { String stringHeaderName = (String) enumerationOfHeaderNames.nextElement(); if (stringHeaderName.equalsIgnoreCase(Utils.CONTENT_LENGTH_HEADER_NAME)) continue; + // //////////////////////////////////////// + // Apply header blacklist: always reject + // //////////////////////////////////////// + + if (headerBlacklist != null && !headerBlacklist.isEmpty()) { + if (headerBlacklist.contains(stringHeaderName.toLowerCase())) { + continue; + } + } + + // //////////////////////////////////////// + // Apply header whitelist: if set, only + // allow headers in the whitelist + // //////////////////////////////////////// + + if (headerWhitelist != null && !headerWhitelist.isEmpty()) { + if (!headerWhitelist.contains(stringHeaderName.toLowerCase())) { + continue; + } + } + // //////////////////////////////////////////////////////////////////////// // As per the Java Servlet API 2.5 documentation: // Some headers, such as Accept-Language can be sent by clients diff --git a/src/main/java/it/geosolutions/httpproxy/ProxyConfig.java b/src/main/java/it/geosolutions/httpproxy/ProxyConfig.java index a527d77..267a0ea 100644 --- a/src/main/java/it/geosolutions/httpproxy/ProxyConfig.java +++ b/src/main/java/it/geosolutions/httpproxy/ProxyConfig.java @@ -65,6 +65,18 @@ final class ProxyConfig { */ private Set hostsWhitelist = new HashSet(); + /** + * A list of request header names (case-insensitive) that the proxy is permitted to forward. + * If non-empty, only headers in this set will be forwarded. + */ + private Set requestHeaderWhitelist = new HashSet(); + + /** + * A list of request header names (case-insensitive) that the proxy must NOT forward. + * Headers in this set will always be removed, even if they appear in the whitelist. + */ + private Set requestHeaderBlacklist = new HashSet(); + /** * The servlet context */ @@ -137,6 +149,14 @@ private void configProxy() { if (p != null) this.setHostsWhitelist(p); + p = Utils.parseWhiteList(props.getProperty("requestHeaderWhitelist")); + if (p != null) + this.setRequestHeaderWhitelist(Utils.toLowerCaseSet(p)); + + p = Utils.parseWhiteList(props.getProperty("requestHeaderBlacklist")); + if (p != null) + this.setRequestHeaderBlacklist(Utils.toLowerCaseSet(p)); + // //////////////////////////////////////// // Read various request type properties // //////////////////////////////////////// @@ -436,6 +456,50 @@ public void setHostsWhitelist(Set hostsWhitelist) { this.hostsWhitelist = hostsWhitelist; } + /** + * @return the requestHeaderWhitelist + */ + public Set getRequestHeaderWhitelist() { + Properties props = propertiesLoader(); + + if (props != null) { + Set set = Utils.parseWhiteList(props.getProperty("requestHeaderWhitelist")); + if (set != null) + this.setRequestHeaderWhitelist(Utils.toLowerCaseSet(set)); + } + + return requestHeaderWhitelist; + } + + /** + * @param requestHeaderWhitelist the requestHeaderWhitelist to set + */ + public void setRequestHeaderWhitelist(Set requestHeaderWhitelist) { + this.requestHeaderWhitelist = requestHeaderWhitelist; + } + + /** + * @return the requestHeaderBlacklist + */ + public Set getRequestHeaderBlacklist() { + Properties props = propertiesLoader(); + + if (props != null) { + Set set = Utils.parseWhiteList(props.getProperty("requestHeaderBlacklist")); + if (set != null) + this.setRequestHeaderBlacklist(Utils.toLowerCaseSet(set)); + } + + return requestHeaderBlacklist; + } + + /** + * @param requestHeaderBlacklist the requestHeaderBlacklist to set + */ + public void setRequestHeaderBlacklist(Set requestHeaderBlacklist) { + this.requestHeaderBlacklist = requestHeaderBlacklist; + } + /** * @return the context */ diff --git a/src/main/java/it/geosolutions/httpproxy/Utils.java b/src/main/java/it/geosolutions/httpproxy/Utils.java index 7670171..bb80bb4 100644 --- a/src/main/java/it/geosolutions/httpproxy/Utils.java +++ b/src/main/java/it/geosolutions/httpproxy/Utils.java @@ -25,7 +25,9 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.HashSet; +import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; /** * Utility methods. @@ -150,6 +152,21 @@ static final Set parseWhiteList(String property) { } } + /** + * Converts all strings in the given set to lower case. + * + * @param set the input set + * @return a new set with all strings converted to lower case + */ + static Set toLowerCaseSet(Set set) { + if (set == null) { + return null; + } + return set.stream().filter(Objects::nonNull) + .map(String::trim).map(String::toLowerCase) + .collect(Collectors.toSet()); + } + static URL buildURL(String value) throws MalformedURLException { URL url = new URL(value); diff --git a/src/main/resources/proxy.properties b/src/main/resources/proxy.properties index 4f70edb..9763994 100644 --- a/src/main/resources/proxy.properties +++ b/src/main/resources/proxy.properties @@ -22,6 +22,14 @@ methodsWhitelist = GET,POST,PUT #hostsWhitelist = 127.0.0.1 +# Optional: only forward these request headers (comma-separated, case-insensitive). +# If empty or commented out, all headers are forwarded (except those in the blacklist). +#requestHeaderWhitelist = Accept,Accept-Language,Content-Type,Authorization,Host + +# Optional: never forward these request headers (comma-separated, case-insensitive). +# Blacklist takes precedence over whitelist. +#requestHeaderBlacklist = Cookie,X-Custom-Secret + #reqtypeWhitelist.capabilities = (([&]?([Rr][Ee][Qq][Uu][Ee][Ss][Tt]=[Gg]et[Cc]apabilities))|([&]?(version=1\\.1\\.1)))+ reqtypeWhitelist.capabilities = .*[Gg]et[Cc]apabilities.* reqtypeWhitelist.featureinfo = .*[Gg]et[Ff]eature[Ii]nfo.* diff --git a/src/test/java/it/geosolutions/httpproxy/RequestHeaderFilterTest.java b/src/test/java/it/geosolutions/httpproxy/RequestHeaderFilterTest.java new file mode 100644 index 0000000..ffd6e6a --- /dev/null +++ b/src/test/java/it/geosolutions/httpproxy/RequestHeaderFilterTest.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2007 - 2011 GeoSolutions S.A.S. + * http://www.geo-solutions.it + * + * GPLv3 + Classpath exception + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package it.geosolutions.httpproxy; + +import java.io.File; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.StatusLine; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.entity.StringEntity; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import static org.mockito.Mockito.*; + +/** + * Tests for request header whitelist and blacklist filtering. + */ +public class RequestHeaderFilterTest extends Mockito { + + Map parameters = new HashMap<>(); + private List
headers = new ArrayList<>(); + + @Before + public void setUp() { + // URL must match one of the reqtypeWhitelist patterns in proxy.properties + parameters.put("url", new String[]{"http://sample.com/csw"}); + } + + /** + * Helper to create a proxy configured with the given properties file resource. + */ + private HTTPProxy createProxy(String propertiesResource, final HttpGet mockGetMethod, + HttpClient mockHttpClient) throws Exception { + File f = new File(getClass().getClassLoader() + .getResource(propertiesResource).getFile()); + + final ServletConfig servletConfig = mock(ServletConfig.class); + ServletContext ctx = mock(ServletContext.class); + when(ctx.getInitParameter("proxyPropPath")).thenReturn(f.getAbsolutePath()); + when(servletConfig.getServletContext()).thenReturn(ctx); + + HTTPProxy proxy = new HTTPProxy() { + private static final long serialVersionUID = 1L; + + @Override + public HttpGet getGetMethod(URL url) { + return mockGetMethod; + } + }; + proxy.setHttpClient(mockHttpClient); + proxy.init(servletConfig); + return proxy; + } + + /** + * Build a mock GET that returns a 200 with the given body. + */ + private HttpGet buildMockGet(HttpClient mockHttpClient) throws Exception { + HttpGet mockGetMethod = mock(HttpGet.class); + HttpResponse response = mock(HttpResponse.class); + StatusLine statusLine = mock(StatusLine.class); + when(statusLine.getStatusCode()).thenReturn(200); + when(response.getStatusLine()).thenReturn(statusLine); + HttpEntity entity = new StringEntity("OK"); + when(response.getEntity()).thenReturn(entity); + when(response.getAllHeaders()).thenReturn(new Header[]{}); + when(mockHttpClient.execute(mockGetMethod)).thenReturn(response); + return mockGetMethod; + } + + @Test + public void testBlacklistRemovesHeaders() throws Exception { + HttpClient mockHttpClient = mock(HttpClient.class); + final HttpGet mockGetMethod = buildMockGet(mockHttpClient); + + HTTPProxy proxy = createProxy("proxy.properties", + mockGetMethod, mockHttpClient); + + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getParameterMap()).thenReturn(parameters); + when(request.getMethod()).thenReturn("GET"); + + // Simulate incoming headers: Accept, X-Secret, Cookie + List headerNames = Arrays.asList("Accept", "X-Secret", "Cookie", "Host"); + when(request.getHeaderNames()).thenReturn(Collections.enumeration(headerNames)); + for (String h : headerNames) { + when(request.getHeaders(h)).thenReturn( + Collections.enumeration(Collections.singletonList("value-" + h))); + } + when(request.getRequestURL()).thenReturn(new StringBuffer("http://proxy.com/proxy")); + + HttpServletResponse response = mock(HttpServletResponse.class); + StubServletOutputStream out = new StubServletOutputStream(); + when(response.getOutputStream()).thenReturn(out); + + proxy.doGet(request, response); + + // X-Secret and Cookie should NOT have been set (blacklisted) + verify(mockGetMethod, never()).setHeader(eq("X-Secret"), anyString()); + verify(mockGetMethod, never()).setHeader(eq("Cookie"), anyString()); + // Accept should have been forwarded + verify(mockGetMethod).setHeader(eq("Accept"), eq("value-Accept")); + // Host is rewritten to the target host + verify(mockGetMethod).setHeader(eq("Host"), anyString()); + } + + @Test + public void testWhitelistOnlyForwardsAllowedHeaders() throws Exception { + HttpClient mockHttpClient = mock(HttpClient.class); + final HttpGet mockGetMethod = buildMockGet(mockHttpClient); + + HTTPProxy proxy = createProxy("proxy.properties", + mockGetMethod, mockHttpClient); + + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getParameterMap()).thenReturn(parameters); + when(request.getMethod()).thenReturn("GET"); + + // Simulate incoming headers: Accept, X-Custom, Authorization, Host, Content-Type + List headerNames = Arrays.asList("Accept", "X-Custom", "Authorization", "Host", "Content-Type"); + when(request.getHeaderNames()).thenReturn(Collections.enumeration(headerNames)); + for (String h : headerNames) { + when(request.getHeaders(h)).thenReturn( + Collections.enumeration(Collections.singletonList("value-" + h))); + } + when(request.getRequestURL()).thenReturn(new StringBuffer("http://proxy.com/proxy")); + + HttpServletResponse response = mock(HttpServletResponse.class); + StubServletOutputStream out = new StubServletOutputStream(); + when(response.getOutputStream()).thenReturn(out); + + proxy.doGet(request, response); + + // Whitelisted: Accept, Content-Type, Host should be forwarded + verify(mockGetMethod).setHeader(eq("Accept"), eq("value-Accept")); + verify(mockGetMethod).setHeader(eq("Content-Type"), eq("value-Content-Type")); + verify(mockGetMethod).setHeader(eq("Host"), anyString()); + // NOT whitelisted: X-Custom, Authorization should be removed + verify(mockGetMethod, never()).setHeader(eq("X-Custom"), anyString()); + verify(mockGetMethod, never()).setHeader(eq("Authorization"), anyString()); + } + + @Test + public void testBlacklistTakesPrecedenceOverWhitelist() throws Exception { + // proxy.properties has both whitelist (Accept,Content-Type,Host) + // and blacklist (X-Secret,Cookie). + // A header NOT in either list should be blocked by the whitelist. + // A header in the blacklist should always be blocked even if it were whitelisted. + HttpClient mockHttpClient = mock(HttpClient.class); + final HttpGet mockGetMethod = buildMockGet(mockHttpClient); + + HTTPProxy proxy = createProxy("proxy.properties", + mockGetMethod, mockHttpClient); + + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getParameterMap()).thenReturn(parameters); + when(request.getMethod()).thenReturn("GET"); + + // Send headers covering all cases: + // Accept -> whitelisted, not blacklisted -> forwarded + // X-Secret -> blacklisted -> blocked + // Authorization -> not whitelisted -> blocked + List headerNames = Arrays.asList("Accept", "X-Secret", "Authorization"); + when(request.getHeaderNames()).thenReturn(Collections.enumeration(headerNames)); + for (String h : headerNames) { + when(request.getHeaders(h)).thenReturn( + Collections.enumeration(Collections.singletonList("value-" + h))); + } + when(request.getRequestURL()).thenReturn(new StringBuffer("http://proxy.com/proxy")); + + HttpServletResponse response = mock(HttpServletResponse.class); + StubServletOutputStream out = new StubServletOutputStream(); + when(response.getOutputStream()).thenReturn(out); + + proxy.doGet(request, response); + + // Accept: whitelisted and not blacklisted -> forwarded + verify(mockGetMethod).setHeader(eq("Accept"), eq("value-Accept")); + // X-Secret: blacklisted -> blocked + verify(mockGetMethod, never()).setHeader(eq("X-Secret"), anyString()); + // Authorization: not in whitelist -> blocked + verify(mockGetMethod, never()).setHeader(eq("Authorization"), anyString()); + } +} diff --git a/src/test/resources/proxy.properties b/src/test/resources/proxy.properties index 7aac378..a661a23 100644 --- a/src/test/resources/proxy.properties +++ b/src/test/resources/proxy.properties @@ -22,6 +22,9 @@ methodsWhitelist = GET,POST,PUT #hostsWhitelist = 127.0.0.1 +requestHeaderBlacklist = X-Secret,Cookie +requestHeaderWhitelist = Accept,Content-Type,Host + #reqtypeWhitelist.capabilities = (([&]?([Rr][Ee][Qq][Uu][Ee][Ss][Tt]=[Gg]et[Cc]apabilities))|([&]?(version=1\\.1\\.1)))+ reqtypeWhitelist.capabilities = .*[Gg]et[Cc]apabilities.* reqtypeWhitelist.featureinfo = .*[Gg]et[Ff]eature[Ii]nfo.* From 7779d2e51214a79941a61db54a7d0109dca1aa93 Mon Sep 17 00:00:00 2001 From: Florian Necas Date: Wed, 25 Mar 2026 12:31:02 +0100 Subject: [PATCH 2/2] feat: update pom.xml to set source and target at 8 --- pom.xml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 5d2de9f..ef7740e 100644 --- a/pom.xml +++ b/pom.xml @@ -29,8 +29,8 @@ maven-compiler-plugin 3.5.1 - 1.7 - 1.7 + 8 + 8 @@ -157,8 +157,6 @@ UTF-8 2.19.0 4.5.13 - 8 - 8