diff --git a/pom.xml b/pom.xml
index 955d423..ef7740e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -29,8 +29,8 @@
maven-compiler-plugin
3.5.1
- 1.7
- 1.7
+ 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.*