Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public enum FeatureId {
moorings_latest_available_date("moorings_latest_available_date"),
mooring_details_between_dates("mooring_details_between_dates"),
wms_map_tile("wms_map_tile"),
wms_legend("wms_legend"), // Get the WMS GetLegendGraphic image (image/png) for a layer
wms_map_feature("wms_map_feature"),
wms_layers("wms_layers"), // Get all available layers from WMS GetCapabilities
wfs_layers("wfs_layers"); // Get all available feature types from WFS GetCapabilities
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class WmsDefaultParam {
private Map<String, String> wms;
private Map<String, String> ncwms;
private Map<String, String> ncmetadata;
private Map<String, String> legend;

private Map<String, String> descLayer;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,77 @@ public byte[] getMapTile(String collectionId, FeatureRequest request) throws URI
return null;
}

/**
* Get the WMS GetLegendGraphic image (image/png) for a layer.
*
* @param collectionId - The uuid
* @param request - The request param, layer name is used
* @return - The legend png bytes, or null if none could be produced
* @throws URISyntaxException - Not expected
*/
public byte[] getLegend(String collectionId, FeatureRequest request) throws URISyntaxException {
Optional<String> mapServerUrl = getMapServerUrl(collectionId, request);
log.debug("legend request for uuid {} layername {}", collectionId, request.getLayerName());
if (mapServerUrl.isPresent()) {
// createLegendUrl validates the user-supplied layer name (SSRF guard) and
// returns null when it is unsafe.
String url = createLegendUrl(mapServerUrl.get(), request);
if (url != null) {
log.debug("legend request for layer name {} url {} ", request.getLayerName(), url);
ResponseEntity<byte[]> response = restTemplateUtils.handleRedirect(url, restTemplate.exchange(url, HttpMethod.GET, pretendUserEntity, byte[].class), byte[].class, pretendUserEntity);
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
// Unlike getMapTile, a non-image response here just means the layer has
// no legend (e.g. ncWMS), so return null (-> 404) rather than throwing.
if (response.getStatusCode().is2xxSuccessful()
&& response.getHeaders().getContentType() != null
&& response.getHeaders().getContentType().getType().equals("image")) {
return response.getBody();
}
}
}
return null;
}

/**
* Build a GetLegendGraphic url against the same host/path as the layer's WMS server.
*
* @param url - The WMS server url from the collection link
* @param request - The request, layer name is used as the LAYER param
* @return - The legend url, or null on syntax error
*/
protected String createLegendUrl(String url, FeatureRequest request) {
String layerName = request.getLayerName();
// SSRF guard: the layer name is the only user-supplied part of the URL, so
// restrict it to safe WMS characters and use this validated value below.
if (layerName == null || !layerName.matches("[A-Za-z0-9_:/.\\-]+")) {
return null;
}
try {
UriComponents components = UriComponentsBuilder.fromUriString(url).build();
if (components.getPath() != null) {
Map<String, String> param = new HashMap<>(wmsDefaultParam.getLegend());
// GetLegendGraphic uses LAYER (singular), unlike GetMap's LAYERS
param.put("LAYER", layerName);

UriComponentsBuilder builder = UriComponentsBuilder
.newInstance()
.scheme("https") // hardcode https to avoid a redirect, as elsewhere
.port(components.getPort())
.host(components.getHost())
.path(components.getPath());

param.forEach((key, value) -> {
if (value != null) {
builder.queryParam(key, value);
}
});
return builder.build().toUriString();
}
} catch (Exception e) {
log.error("URL syntax error {}", url, e);
}
return null;
}

/**
* Query by using WMS's DescriberLayer function to find out the associated WFS layer and fields
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ public ResponseEntity<?> getFeature(
case wms_map_tile -> {
return featuresService.getWmsMapTile(collectionId, request);
}
case wms_legend -> {
return featuresService.getWmsLegend(collectionId, request);
}
case wms_map_feature -> {
return featuresService.getWmsMapFeature(collectionId, request);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
Expand All @@ -36,6 +37,7 @@
import java.io.IOException;
import java.io.StringReader;
import java.net.URISyntaxException;
import java.time.Duration;
import java.util.*;

@Slf4j
Expand Down Expand Up @@ -107,6 +109,29 @@ public ResponseEntity<byte[]> getWmsMapTile(String collectionId, FeatureRequest
}
}

/**
* Proxy the WMS GetLegendGraphic image (colour-bar / symbol legend) for a layer.
*
* @param collectionId - The uuid of the dataset
* @param request - Request holding the layer name
* @return - The legend as an image/png, or 404 if no legend could be built
*/
public ResponseEntity<byte[]> getWmsLegend(String collectionId, FeatureRequest request) {
try {
byte[] legend = wmsServer.getLegend(collectionId, request);
if (legend == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok()
.contentType(MediaType.IMAGE_PNG)
// Legends are stable per layer/style, so let the browser/CDN cache them
.cacheControl(CacheControl.maxAge(Duration.ofHours(24)).cachePublic())
.body(legend);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}

/**
* This is used to get the WFS fields given a WFS layer
*
Expand Down
6 changes: 6 additions & 0 deletions server/src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ wms-default-param:
VERSION: "1.3.0"
# Must be small letter item !!!
item: "layerDetails"
legend:
SERVICE: "WMS"
VERSION: "1.1.1"
REQUEST: "GetLegendGraphic"
FORMAT: "image/png"
LEGEND_OPTIONS: "forceLabels:on"
allow-id: # Full list
- 4402cb50-e20a-44ee-93e6-4728259250d2
- ae86e2f5-eaaf-459e-a405-e654d85adb9c
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.web.client.RestTemplate;
Expand Down Expand Up @@ -675,4 +678,71 @@ void testRewriteUrlWithWorkSpace() {
String result4 = WmsServer.rewriteUrlWithWorkSpace(url4, req);
assertEquals("https://example.com/geoserver/xxx/wfs", result4);
}

@Test
public void verifyLegendUrlGenCorrect() {
FeatureRequest request = FeatureRequest.builder()
.layerName("imos:argo_profile_map")
.build();

String url = wmsServer.createLegendUrl("http://geoserver-123.aodn.org.au/geoserver/wms", request);
assertNotNull(url);

// Legend is forced to https, uses REQUEST=GetLegendGraphic and the default image/png
String expectedUrl = "https://geoserver-123.aodn.org.au/geoserver/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetLegendGraphic&FORMAT=image/png&LEGEND_OPTIONS=forceLabels:on&LAYER=imos:argo_profile_map";
UriComponents expected = UriComponentsBuilder.fromUriString(expectedUrl).build();
UriComponents result = UriComponentsBuilder.fromUriString(url).build();

assertEquals(expected.getScheme(), result.getScheme());
assertEquals(expected.getHost(), result.getHost());
assertEquals(expected.getPath(), result.getPath());

expected.getQueryParams()
.forEach((key, value) ->
assertEquals(value, result.getQueryParams().get(key), key)
);

// GetLegendGraphic uses LAYER (singular), never GetMap's LAYERS
assertNull(result.getQueryParams().get("LAYERS"));
}

@Test
public void createLegendUrl_rejectsUnsafeLayerName() {
String base = "https://geoserver-123.aodn.org.au/geoserver/wms";
// A normal layer name builds a url
assertNotNull(wmsServer.createLegendUrl(base,
FeatureRequest.builder().layerName("imos:argo_profile_map").build()));
// A crafted layer name is rejected (SSRF guard) -> null
assertNull(wmsServer.createLegendUrl(base,
FeatureRequest.builder().layerName("x?SERVICE=WMS&url=http://evil").build()));
}

@Test
public void getLegend_returnsImageBytesFromServer() {
String id = "id";
FeatureRequest request = FeatureRequest.builder().layerName("imos:argo_profile_map").build();

ElasticSearchBase.SearchResult<StacCollectionModel> stac = new ElasticSearchBase.SearchResult<>();
stac.setCollections(new ArrayList<>());
stac.getCollections().add(
StacCollectionModel.builder()
.links(List.of(
LinkModel.builder()
.href("http://geoserver-123.aodn.org.au/geoserver/wms")
.title(request.getLayerName())
.aiGroup(WMS_LINK_MARKER)
.build()))
.build()
);
when(search.searchCollections(eq(id))).thenReturn(stac);

byte[] png = new byte[]{(byte) 0x89, 'P', 'N', 'G'};
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.IMAGE_PNG);
when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(entity), eq(byte[].class)))
.thenReturn(new ResponseEntity<>(png, headers, HttpStatus.OK));

byte[] result = assertDoesNotThrow(() -> wmsServer.getLegend(id, request));
assertArrayEquals(png, result);
}
}
Loading