Skip to content

Commit

Permalink
[GEOS-9816] Download links from the result of an asynch process will …
Browse files Browse the repository at this point in the history
…not honor the proxy base URL, if it uses HTTP header variables
  • Loading branch information
aaime committed Dec 3, 2020
1 parent 8dced93 commit 35b060c
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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"));
}
}
12 changes: 9 additions & 3 deletions src/main/src/main/java/applicationContext.xml
Original file line number Diff line number Diff line change
Expand Up @@ -232,12 +232,18 @@
<constructor-arg ref="catalog"/>
</bean>

<!-- the proxyfing URL mangler -->
<!-- the proxyfing URL mangler, along with machinery to support headers expansion usage in asynch requests -->
<bean id="proxyfier" class="org.geoserver.ows.ProxifyingURLMangler">
<constructor-arg index="0" ref="geoServer"/>
</bean>

<!-- URL mangler for workspace/layers accessed through the /ows?service=... service end points -->
<bean id="proxyfierHeaderCollector" class="org.geoserver.ows.HTTPHeadersCollector"/>
<bean id="proxyfierHeaderTransfer" class="org.geoserver.threadlocals.PublicThreadLocalTransfer">
<constructor-arg index="0" value="org.geoserver.ows.HTTPHeadersCollector"/>
<constructor-arg index="1" value="HEADERS"/>
</bean>


<!-- URL mangler for workspace/layers accessed through the /ows?service=... service end points -->
<bean id="owsDispatcherLocalWorkspaceURLManger" class="org.geoserver.ows.LocalWorkspaceURLMangler">
<constructor-arg value="ows"/>
</bean>
Expand Down
51 changes: 51 additions & 0 deletions src/main/src/main/java/org/geoserver/ows/HTTPHeadersCollector.java
Original file line number Diff line number Diff line change
@@ -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<Map<String, String>> HEADERS = new ThreadLocal<>();

@Override
public Request init(Request request) {
HttpServletRequest hr = request.getHttpRequest();
Enumeration<String> names = hr.getHeaderNames();
Map<String, String> 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<String, String> headers = HEADERS.get();
if (headers == null) return null;
return headers.get(header);
}
}
65 changes: 27 additions & 38 deletions src/main/src/main/java/org/geoserver/ows/ProxifyingURLMangler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -157,45 +156,35 @@ private StringBuilder mangleURLHeaders(StringBuilder baseURL, String proxyBase)
* @return map of header names and values
*/
private Map<String, String> compileHeadersMap() {
Map<String, String> headers = new HashMap<>();
Arrays.asList(Headers.values()).forEach(h -> collectHeader(headers, h.asString()));

Map<String, String> headers = new HashMap<String, String>();
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<String, String> 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<String, String> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@
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;
import org.geoserver.config.GeoServer;
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;
Expand Down Expand Up @@ -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<String> 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())
Expand Down

0 comments on commit 35b060c

Please sign in to comment.