From 35b060c38bff8bb1fd084e0fe948b9842621e949 Mon Sep 17 00:00:00 2001 From: Andrea Aime Date: Wed, 2 Dec 2020 18:07:50 +0100 Subject: [PATCH] [GEOS-9816] Download links from the result of an asynch process will not honor the proxy base URL, if it uses HTTP header variables --- .../java/org/geoserver/wps/ExecuteTest.java | 71 +++++++++++++++++-- src/main/src/main/java/applicationContext.xml | 12 +++- .../geoserver/ows/HTTPHeadersCollector.java | 51 +++++++++++++ .../geoserver/ows/ProxifyingURLMangler.java | 65 +++++++---------- .../geoserver/util/URLProxifyingTest.java | 11 ++- 5 files changed, 162 insertions(+), 48 deletions(-) create mode 100644 src/main/src/main/java/org/geoserver/ows/HTTPHeadersCollector.java diff --git a/src/extension/wps/wps-core/src/test/java/org/geoserver/wps/ExecuteTest.java b/src/extension/wps/wps-core/src/test/java/org/geoserver/wps/ExecuteTest.java index 67a712512b1..382dd01af2a 100644 --- a/src/extension/wps/wps-core/src/test/java/org/geoserver/wps/ExecuteTest.java +++ b/src/extension/wps/wps-core/src/test/java/org/geoserver/wps/ExecuteTest.java @@ -40,6 +40,9 @@ import org.custommonkey.xmlunit.exceptions.XpathException; import org.geoserver.catalog.LayerInfo; import org.geoserver.catalog.ResourceInfo; +import org.geoserver.config.GeoServer; +import org.geoserver.config.GeoServerInfo; +import org.geoserver.config.SettingsInfo; import org.geoserver.data.test.MockData; import org.geoserver.data.test.SystemTestData; import org.geoserver.data.test.SystemTestData.LayerProperty; @@ -66,6 +69,7 @@ import org.geotools.referencing.crs.DefaultGeographicCRS; import org.geotools.util.PreventLocalEntityResolver; import org.geotools.xsd.Parser; +import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -74,6 +78,7 @@ import org.locationtech.jts.geom.Polygon; import org.locationtech.jts.io.WKTReader; import org.opengis.feature.simple.SimpleFeatureType; +import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.w3c.dom.Document; @@ -1219,9 +1224,7 @@ public void testStoredNoStatus() throws Exception { "wps?service=WPS&version=1.0.0&request=Execute&Identifier=gs:Monkey&storeExecuteResponse=true&DataInputs=" + urlEncode("id=x2"); Document dom = getAsDOM(request); - assertXpathExists("//wps:ProcessAccepted", dom); - XpathEngine xpath = XMLUnit.newXpathEngine(); - String fullStatusLocation = xpath.evaluate("//wps:ExecuteResponse/@statusLocation", dom); + String fullStatusLocation = getFullStatusLocation(dom); String statusLocation = fullStatusLocation.substring(fullStatusLocation.indexOf('?') - 3); // we move the clock forward, but we asked no status, nothing should change @@ -2043,13 +2046,17 @@ private String submitMonkey(String id) throws Exception, XpathException { } private String getStatusLocation(Document dom) throws XpathException { - assertXpathExists("//wps:ProcessAccepted", dom); - XpathEngine xpath = XMLUnit.newXpathEngine(); - String fullStatusLocation = xpath.evaluate("//wps:ExecuteResponse/@statusLocation", dom); + String fullStatusLocation = getFullStatusLocation(dom); String statusLocation = fullStatusLocation.substring(fullStatusLocation.indexOf('?') - 3); return statusLocation; } + private String getFullStatusLocation(Document dom) throws XpathException { + assertXpathExists("//wps:ProcessAccepted", dom); + XpathEngine xpath = XMLUnit.newXpathEngine(); + return xpath.evaluate("//wps:ExecuteResponse/@statusLocation", dom); + } + private ListFeatureCollection collectionOfThings() { SimpleFeatureType featureType = buildSampleFeatureType(); ListFeatureCollection fc = new ListFeatureCollection(featureType); @@ -2185,4 +2192,56 @@ private void enableWPSOnStreams() { getCatalog().save(ri); getCatalog().save(linfo); } + + @Test + public void testBacklinksProxyHeaders() throws Exception { + GeoServer gs = getGeoServer(); + GeoServerInfo gsInfo = gs.getGlobal(); + gsInfo.setUseHeadersProxyURL(true); + SettingsInfo settings = gsInfo.getSettings(); + settings.setProxyBaseUrl("${X-Forwarded-Proto}://${X-Forwarded-Host}/geoserver"); + gs.save(gsInfo); + + // submit asynch request with proxy headers + String wpsRequest = + "wps?service=WPS&version=1.0.0&request=Execute&Identifier=gs:Monkey&storeExecuteResponse=true&status=true&storeExecuteResponse=true&DataInputs=" + + urlEncode("id=proxyHeaders") + + "&ResponseDocument=" + + urlEncode("result=@asReference=true"); + MockHttpServletRequest hreq = createRequest(wpsRequest); + hreq.setMethod("GET"); + hreq.setContent(new byte[] {}); + hreq.addHeader("X-Forwarded-Proto", "https"); + hreq.addHeader("X-Forwarded-Host", "mycompany.com"); + + MockHttpServletResponse response = dispatch(hreq, null); + InputStream responseContent = + new ByteArrayInputStream(response.getContentAsString().getBytes()); + Document dom = dom(responseContent, true); + // print(dom); + String fullStatusLocation = getFullStatusLocation(dom); + String statusLocation = fullStatusLocation.substring(fullStatusLocation.indexOf('?') - 3); + assertThat(fullStatusLocation, CoreMatchers.startsWith("https://mycompany.com/geoserver")); + + // pretend we are are container and clean up the HTTP Headers + hreq.removeHeader("X-Forwarded-Proto"); + hreq.removeHeader("X-Forwarded-Host"); + + // now schedule the exit and wait for it to exit + MonkeyProcess.exit("proxyHeaders", collectionOfThings(), true); + dom = waitForProcessEnd(statusLocation, 60); + // print(dom); + assertXpathExists("//wps:ProcessSucceeded", dom); + + // The document has been encoded as the final stage of the async execution, in a background + // thread. Check it is still using the right proxy variables + XpathEngine xpath = XMLUnit.newXpathEngine(); + fullStatusLocation = xpath.evaluate("//wps:ExecuteResponse/@statusLocation", dom); + assertThat(fullStatusLocation, CoreMatchers.startsWith("https://mycompany.com/geoserver")); + String reference = + xpath.evaluate( + "//wps:ExecuteResponse/wps:ProcessOutputs/wps:Output/wps:Reference/@href", + dom); + assertThat(reference, CoreMatchers.startsWith("https://mycompany.com/geoserver")); + } } diff --git a/src/main/src/main/java/applicationContext.xml b/src/main/src/main/java/applicationContext.xml index 9acf64a9db2..3425d51fb6f 100644 --- a/src/main/src/main/java/applicationContext.xml +++ b/src/main/src/main/java/applicationContext.xml @@ -232,12 +232,18 @@ - + - - + + + + + + + + diff --git a/src/main/src/main/java/org/geoserver/ows/HTTPHeadersCollector.java b/src/main/src/main/java/org/geoserver/ows/HTTPHeadersCollector.java new file mode 100644 index 00000000000..773cb1a690d --- /dev/null +++ b/src/main/src/main/java/org/geoserver/ows/HTTPHeadersCollector.java @@ -0,0 +1,51 @@ +/* (c) 2020 Open Source Geospatial Foundation - all rights reserved + * This code is licensed under the GPL 2.0 license, available at the root + * application directory. + */ +package org.geoserver.ows; + +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import org.geoserver.ows.util.CaseInsensitiveMap; + +/** + * Collects headers on behalf of {@link ProxifyingURLMangler}, so that they can be used also for + * asynchronoous executions happening outside of request threads. Given the specific usage, only the + * first value of headers is collected. + */ +public class HTTPHeadersCollector extends AbstractDispatcherCallback { + + public static final ThreadLocal> HEADERS = new ThreadLocal<>(); + + @Override + public Request init(Request request) { + HttpServletRequest hr = request.getHttpRequest(); + Enumeration names = hr.getHeaderNames(); + Map headers = new CaseInsensitiveMap(new HashMap<>()); + while (names.hasMoreElements()) { + String header = names.nextElement(); + String value = hr.getHeader(header); + headers.put(header, value); + } + HEADERS.set(headers); + + return request; + } + + @Override + public void finished(Request request) { + HEADERS.remove(); + } + + /** + * Returns the value for the specified header, if the {@link #HEADERS} thread local is loaded, + * and contains one, null otherwise. + */ + public static String getHeader(String header) { + Map headers = HEADERS.get(); + if (headers == null) return null; + return headers.get(header); + } +} diff --git a/src/main/src/main/java/org/geoserver/ows/ProxifyingURLMangler.java b/src/main/src/main/java/org/geoserver/ows/ProxifyingURLMangler.java index 8e53009ae78..b253ef6f390 100644 --- a/src/main/src/main/java/org/geoserver/ows/ProxifyingURLMangler.java +++ b/src/main/src/main/java/org/geoserver/ows/ProxifyingURLMangler.java @@ -10,7 +10,6 @@ import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.servlet.http.HttpServletRequest; import org.geoserver.config.GeoServer; import org.geoserver.platform.GeoServerExtensions; import org.vfny.geoserver.util.Requests; @@ -157,45 +156,35 @@ private StringBuilder mangleURLHeaders(StringBuilder baseURL, String proxyBase) * @return map of header names and values */ private Map compileHeadersMap() { + Map headers = new HashMap<>(); + Arrays.asList(Headers.values()).forEach(h -> collectHeader(headers, h.asString())); - Map headers = new HashMap(); + return headers; + } - HttpServletRequest owsRequest = Dispatcher.REQUEST.get().getHttpRequest(); - Arrays.asList(Headers.values()) - .forEach( - (header) -> { - if (owsRequest.getHeader(header.asString()) != null) { - if (header == Headers.FORWARDED) { - FORWARDED_PATTERNS.forEach( - (comp, pattern) -> { - Matcher m = - pattern.matcher( - owsRequest.getHeader( - header.asString())); - if (m.matches()) { - headers.put( - String.format( - "%s%s%s", - TEMPLATE_PREFIX, - Headers.FORWARDED.asString() - + "." - + comp, - TEMPLATE_POSTFIX), - m.group(2)); - } - }); - } else { - headers.put( - String.format( - "%s%s%s", - TEMPLATE_PREFIX, - header.asString(), - TEMPLATE_POSTFIX), - owsRequest.getHeader(header.asString())); - } - } - }); + private void collectHeader(Map headers, String headerName) { + String headerValue = HTTPHeadersCollector.getHeader(headerName); + if (headerValue != null) { + if (headerName.equals(Headers.FORWARDED.asString())) { + collectForwardedHeaders(headers, headerValue); + } else { + headers.put(toTemplate(headerName), headerValue); + } + } + } - return headers; + private void collectForwardedHeaders(Map headers, String headerValue) { + FORWARDED_PATTERNS.forEach( + (comp, pattern) -> { + Matcher m = pattern.matcher(headerValue); + if (m.matches()) { + String key = toTemplate(Headers.FORWARDED.asString() + "." + comp); + headers.put(key, m.group(2)); + } + }); + } + + private String toTemplate(String header) { + return String.format("%s%s%s", TEMPLATE_PREFIX, header, TEMPLATE_POSTFIX); } } diff --git a/src/main/src/test/java/org/vfny/geoserver/util/URLProxifyingTest.java b/src/main/src/test/java/org/vfny/geoserver/util/URLProxifyingTest.java index 3edf4fddd51..079e246bd55 100644 --- a/src/main/src/test/java/org/vfny/geoserver/util/URLProxifyingTest.java +++ b/src/main/src/test/java/org/vfny/geoserver/util/URLProxifyingTest.java @@ -8,6 +8,8 @@ import static org.easymock.EasyMock.*; import static org.junit.Assert.assertEquals; +import java.util.Collections; +import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import javax.servlet.http.HttpServletRequestWrapper; @@ -15,6 +17,7 @@ import org.geoserver.config.SettingsInfo; import org.geoserver.config.impl.GeoServerInfoImpl; import org.geoserver.ows.Dispatcher; +import org.geoserver.ows.HTTPHeadersCollector; import org.geoserver.ows.ProxifyingURLMangler; import org.geoserver.ows.Request; import org.geoserver.ows.URLMangler; @@ -58,12 +61,18 @@ void createAppContext( .andReturn( new HttpServletRequestWrapper(new MockHttpServletRequest()) { public String getHeader(String name) { - return headers.get(name.toLowerCase()); + return headers.get(name); + } + + @Override + public Enumeration getHeaderNames() { + return Collections.enumeration(headers.keySet()); } }) .anyTimes(); replay(request); Dispatcher.REQUEST.set(request); + new HTTPHeadersCollector().init(request); GeoServer geoServer = createNiceMock(GeoServer.class); expect(geoServer.getGlobal())