Skip to content

Commit

Permalink
[Fixes #8461] WCS getCoverage links have wrong params (#8472)
Browse files Browse the repository at this point in the history
(cherry picked from commit 3cd6a92)
  • Loading branch information
Alessio Fabiani authored Dec 9, 2021
1 parent bcefdc4 commit f804ace
Show file tree
Hide file tree
Showing 3 changed files with 240 additions and 17 deletions.
215 changes: 206 additions & 9 deletions geonode/geoserver/ows.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,20 @@
#
#########################################################################

import ast
import typing
import logging

from django.conf import settings
from django.urls import reverse
from django.utils.translation import ugettext as _

from lxml import etree
from urllib.parse import urlencode, urljoin
from .helpers import OGC_Servers_Handler
from geonode.utils import (
XML_PARSER,
http_client)

logger = logging.getLogger(__name__)

Expand All @@ -34,6 +40,24 @@
DEFAULT_EXCLUDE_FORMATS = ['PNG', 'JPEG', 'GIF', 'TIFF']


def _get_nsmap(original: typing.Dict, key: str):
"""Prepare namespaces dict for running xpath queries.
lxml complains when a namespaces dict has an entry with None as a key.
"""

result = original.copy()
try:
result[key] = original[None]
except KeyError:
pass
else:
del result[None]

return result


def _wcs_get_capabilities():
try:
wcs_url = urljoin(settings.SITEURL, reverse('ows_endpoint'))
Expand All @@ -48,29 +72,202 @@ def _wcs_get_capabilities():
})


def _wcs_link(wcs_url, identifier, mime, srid=None, bbox=None):
def _wcs_describe_coverage(coverage_id):
try:
wcs_url = urljoin(settings.SITEURL, reverse('ows_endpoint'))
except Exception:
wcs_url = urljoin(ogc_settings.PUBLIC_LOCATION, 'ows')
wcs_url += '&' if '?' in wcs_url else '?'
return wcs_url + urlencode({
'service': 'WCS',
'request': 'describecoverage',
'version': '2.0.1',
'coverageid': coverage_id
})


def _get_wcs_axis_labels(coverage_id):
"""
This is a utiliy method using the GeoNode Proxy to fetch the "DescribeCoverage".
The outcome will be used to fetch the "Envelope" "axisLabels", that the WCS declares accordingly to the provided "outputCrs".
The response will be something like the following here below:
<?xml version="1.0" encoding="UTF-8"?>
<wcs:CoverageDescriptions
xmlns:wcs="http://www.opengis.net/wcs/2.0" xmlns:ows="http://www.opengis.net/ows/2.0"
xmlns:gml="http://www.opengis.net/gml/3.2" xmlns:gmlcov="http://www.opengis.net/gmlcov/1.0"
xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:swe="http://www.opengis.net/swe/2.0" xmlns:wcsgs="http://www.geoserver.org/wcsgs/2.0"
xsi:schemaLocation="
http://www.opengis.net/wcs/2.0 http://schemas.opengis.net/wcs/2.0/wcsDescribeCoverage.xsd
http://www.geoserver.org/wcsgs/2.0
http://localhost:8080/geoserver/schemas/wcs/2.0/wcsgs.xsd">
<wcs:CoverageDescription gml:id="geonode__gebco_2020_n54_0659179519862_s51_64453104883433_w0_5559082031249998_e4_409911975264549">
<gml:description>Generated from WorldImage</gml:description>
<gml:name>gebco_2020_n54.0659179519862_s51.64453104883433_w0.5559082031249998_e4.409911975264549</gml:name>
<gml:boundedBy>
<gml:Envelope srsName="http://www.opengis.net/def/crs/EPSG/0/404000" axisLabels="x y" uomLabels="m m" srsDimension="2">
<gml:lowerCorner>-0.5 -0.5</gml:lowerCorner>
<gml:upperCorner>580.5 924.5</gml:upperCorner>
</gml:Envelope>
</gml:boundedBy>
<wcs:CoverageId>geonode__gebco_2020_n54_0659179519862_s51_64453104883433_w0_5559082031249998_e4_409911975264549</wcs:CoverageId>
<gml:coverageFunction>
<gml:GridFunction>
<gml:sequenceRule axisOrder="+2 +1">Linear</gml:sequenceRule>
<gml:startPoint>0 0</gml:startPoint>
</gml:GridFunction>
</gml:coverageFunction>
<gmlcov:metadata>
<gmlcov:Extension>
<ows:Keywords>
<ows:Keyword>gebco_2020_n54.0659179519862_s51.64453104883433_w0.5559082031249998_e4.409911975264549</ows:Keyword>
<ows:Keyword>WCS</ows:Keyword>
<ows:Keyword>WorldImage</ows:Keyword>
</ows:Keywords>
<ows:Metadata xlink:type="simple" xlink:href="..."/>
<ows:Metadata xlink:type="simple" xlink:href="..."/>
<ows:Metadata xlink:type="simple" xlink:href="..."/>
<ows:Metadata xlink:type="simple" xlink:href="..."/>
<ows:Metadata xlink:type="simple" xlink:href="..."/>
<ows:Metadata xlink:type="simple" xlink:href="..."/>
<ows:Metadata xlink:type="simple" xlink:href="http://localhost:8000/showmetadata/xsl/42"/>
</gmlcov:Extension>
</gmlcov:metadata>
<gml:domainSet>
<gml:RectifiedGrid gml:id="grid00__geonode__gebco_2020_n54_0659179519862_s51_64453104883433_w0_5559082031249998_e4_409911975264549" dimension="2">
<gml:limits>
<gml:GridEnvelope>
<gml:low>0 0</gml:low>
<gml:high>924 580</gml:high>
</gml:GridEnvelope>
</gml:limits>
<gml:axisLabels>i j</gml:axisLabels>
<gml:origin>
<gml:Point gml:id="p00_geonode__gebco_2020_n54_0659179519862_s51_64453104883433_w0_5559082031249998_e4_409911975264549" srsName="http://www.opengis.net/def/crs/EPSG/0/404000">
<gml:pos>0.0 0.0</gml:pos>
</gml:Point>
</gml:origin>
<gml:offsetVector srsName="http://www.opengis.net/def/crs/EPSG/0/404000">0.0 1.0</gml:offsetVector>
<gml:offsetVector srsName="http://www.opengis.net/def/crs/EPSG/0/404000">1.0 0.0</gml:offsetVector>
</gml:RectifiedGrid>
</gml:domainSet>
<gmlcov:rangeType>
<swe:DataRecord>
<swe:field name="GRAY_INDEX">
<swe:Quantity>
<swe:description>GRAY_INDEX</swe:description>
<swe:uom code="W.m-2.Sr-1"/>
<swe:constraint>
<swe:AllowedValues>
<swe:interval>0 65535</swe:interval>
</swe:AllowedValues>
</swe:constraint>
</swe:Quantity>
</swe:field>
</swe:DataRecord>
</gmlcov:rangeType>
<wcs:ServiceParameters>
<wcs:CoverageSubtype>RectifiedGridCoverage</wcs:CoverageSubtype>
<wcs:nativeFormat>image/tiff</wcs:nativeFormat>
</wcs:ServiceParameters>
</wcs:CoverageDescription>
</wcs:CoverageDescriptions>
"""

def _swap(_axis_labels):
# WCS 2.0.1 swaps the order of the Lon/Lat axis to Lat/Lon.
if _axis_labels[0].lower() in 'lat':
return [_axis_labels[1], _axis_labels[0]]
else:
return _axis_labels

try:
_describe_coverage_response, _content = http_client.get(_wcs_describe_coverage(coverage_id))
_describe_coverage_response.raise_for_status()
_root = etree.fromstring(_content.encode('UTF-8'), parser=XML_PARSER)
_nsmap = _get_nsmap(_root.nsmap, 'wcs')
_coverage_descriptions = _root.xpath("//wcs:CoverageDescription", namespaces=_nsmap)
for _coverage_description in _coverage_descriptions:
_envelope = _coverage_description.xpath("gml:boundedBy/gml:Envelope", namespaces=_nsmap)
_axis_labels = _envelope[0].attrib["axisLabels"].split(" ")
if _axis_labels and isinstance(_axis_labels, list) and len(_axis_labels) == 2:
return _swap(_axis_labels)
except Exception as e:
logger.error(e)
return None


def _wcs_link(wcs_url, identifier, mime, srid=None, bbox=None, compression=None, tile_size=None):
"""
Utility method generating a "GetCoverage" URL by fixing some parameters, like the "axisLabels"
e.g.:
http://localhost:8080/geoserver/ows?
service=WCS&
request=GetCoverage&
coverageid=geonode__relief_san_andres0&
format=image%2Ftiff&
version=2.0.1&
compression=DEFLATE&
tileWidth=512&
tileHeight=512&
outputCrs=EPSG%3A4326&
subset=Long(-84.0,-78.0)&
subset=Lat(11.0,15.0)
"""
coverage_id = identifier.replace(':', '__', 1)
wcs_params = {
'service': 'WCS',
'request': 'GetCoverage',
'coverageid': identifier.replace(':', '__', 1),
'coverageid': coverage_id,
'format': mime,
'version': '2.0.1',
}

if compression:
wcs_params['compression'] = compression

if tile_size:
wcs_params['tileWidth'] = tile_size
wcs_params['tileHeight'] = tile_size

if srid:
wcs_params['srs'] = srid
wcs_params['outputCrs'] = srid

_wcs_params = urlencode(wcs_params)

if bbox:
wcs_params['bbox'] = bbox
return wcs_url + urlencode(wcs_params)
_bbox = None
if isinstance(bbox, list):
_bbox = bbox
elif isinstance(bbox, str):
_bbox = ast.literal_eval(f'[{bbox}]') if all([_x in bbox for _x in ['[', ']']]) else ast.literal_eval(f'[{bbox}]')
if _bbox:
_axis_labels = _get_wcs_axis_labels(coverage_id)
if _axis_labels:
_wcs_params += f'&subset={_axis_labels[0]}({_bbox[0]},{_bbox[2]})&subset={_axis_labels[1]}({_bbox[1]},{_bbox[3]})'
return f'{wcs_url}{_wcs_params}'


def wcs_links(wcs_url, identifier, bbox=None, srid=None):
"""
Creates the WCS GetCoverage Default download links.
By providing 'None' bbox and srid, we are going to ask to the WCS to
skip subsetting, i.e. output the whole coverage in the netive SRS.
Notice that the "wcs_links" method also generates 1 default "outputFormat":
- "geotiff"; GeoTIFF which will be compressed and tiled by passing to the WCS the default query params compression='DEFLATE' and tile_size=512
"""
types = [
("x-gzip", _("GZIP"), "application/x-gzip"),
("geotiff", _("GeoTIFF"), "image/tiff"),
# AF: Slow outputFormat, removed -> ("x-gzip", _("GZIP"), "application/x-gzip", None, None),
("geotiff", _("GeoTIFF"), "image/tiff", "DEFLATE", 512),
]
output = []
for ext, name, mime in types:
url = _wcs_link(wcs_url, identifier, mime, bbox=bbox, srid=srid)
for ext, name, mime, compression, tile_size in types:
url = _wcs_link(wcs_url, identifier, mime, bbox=bbox, srid=srid, compression=compression, tile_size=tile_size)
output.append((ext, name, mime, url))
return output

Expand Down
11 changes: 7 additions & 4 deletions geonode/geoserver/tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -978,18 +978,21 @@ def test_ogc_server_defaults(self):

# WCS Links
wcs_links = wcs_links(f"{ogc_settings.public_url}wcs?",
instance.alternate,
bbox,
srid)
instance.alternate)
self.assertIsNotNone(wcs_links)
self.assertEqual(len(wcs_links), 2)
self.assertEqual(len(wcs_links), 1)
wcs_url = urljoin(ogc_settings.PUBLIC_LOCATION, 'wcs')
identifier = urlencode({'coverageid': instance.alternate.replace(':', '__', 1)})
for _link in wcs_links:
logger.debug(f'{wcs_url} --> {_link[3]}')
self.assertTrue(wcs_url in _link[3])
logger.debug(f'{identifier} --> {_link[3]}')
self.assertTrue(identifier in _link[3])
if srid:
self.assertFalse('outputCrs' in _link[3])
if bbox:
self.assertFalse('subset=Long' in _link[3])
self.assertFalse('subset=Lat' in _link[3])

@on_ogc_backend(geoserver.BACKEND_PACKAGE)
def test_importer_configuration(self):
Expand Down
31 changes: 27 additions & 4 deletions geonode/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#########################################################################

import os
import gc
import re
Expand All @@ -28,6 +29,7 @@
import select
import shutil
import string
import typing
import logging
import tarfile
import datetime
Expand All @@ -36,6 +38,7 @@
import traceback
import subprocess

from lxml import etree
from osgeo import ogr
from PIL import Image
from io import BytesIO, StringIO
Expand Down Expand Up @@ -102,6 +105,9 @@
'content-disposition'
]

# explicitly disable resolving XML entities in order to prevent malicious attacks
XML_PARSER: typing.Final = etree.XMLParser(resolve_entities=False)

requests.packages.urllib3.disable_warnings()

signalnames = [
Expand Down Expand Up @@ -1825,10 +1831,16 @@ def set_resource_default_links(instance, layer, prune=False, **kwargs):
)

elif instance.storeType == 'coverageStore':
links = wcs_links(f"{ogc_server_settings.public_url}wcs?",
instance.alternate,
bbox,
srid)
"""
Going to create the WCS GetCoverage Default download links.
By providing 'None' bbox and srid, we are going to ask to the WCS to
skip subsetting, i.e. output the whole coverage in the netive SRS.
Notice that the "wcs_links" method also generates 1 default "outputFormat":
- "geotiff"; GeoTIFF which will be compressed and tiled by passing to the WCS the default query params compression='DEFLATE' and tile_size=512
"""
links = wcs_links(f"{ogc_server_settings.public_url}ows?",
instance.alternate)

for ext, name, mime, wcs_url in links:
if (Link.objects.filter(resource=instance.resourcebase_ptr,
Expand Down Expand Up @@ -2141,3 +2153,14 @@ def find_by_attr(lst, val, attr="id"):
return item

return None


def get_xpath_value(
element: etree.Element,
xpath_expression: str,
nsmap: typing.Optional[dict] = None
) -> typing.Optional[str]:
if not nsmap:
nsmap = element.nsmap
values = element.xpath(f"{xpath_expression}//text()", namespaces=nsmap)
return "".join(values).strip() or None

0 comments on commit f804ace

Please sign in to comment.