Fix POST requests for QGIS server

Followup https://github.com/qgis/QGIS/pull/8830 that fixed
a regression with rewritten urls in the server, unfortunately
my original solution introduced a side-effect on the POST
request, with the new approach I'm introducing a new method
to retrieve the URL as seen by the web server: by default
this is the same URL seen by QGIS server, but in case
a rewrite module made some changes, the original URL will
be used as a base URL if not overridden by a config setting.

This PR comes with an extended set of tests that should
cover both (rewritten and unrewritten) cases for GET and
POST and for WFS/WFS/WCS and WMTS.
This commit is contained in:
Alessandro Pasotti 2019-01-21 18:58:25 +01:00
parent 1b11ba43a2
commit 691176b39b
12 changed files with 241 additions and 106 deletions

View File

@ -32,20 +32,6 @@ Class defining fcgi request
Returns true if an error occurred during initialization
%End
virtual QUrl url() const;
%Docstring
:return: the request url
Overrides base implementation because FCGI is typically behind
a proxy server and QGIS Server will see a rewritten QUERY_STRING.
FCGI implementation stores the REQUEST_URI (which is the URL seen
by the proxy before it gets rewritten) and returns it instead of
the rewritten one.
%End
};
/************************************************************************

View File

@ -56,14 +56,13 @@ Constructor
virtual ~QgsServerRequest();
virtual QUrl url() const;
QUrl url() const;
%Docstring
:return: the request url
:return: the request url as seen by QGIS server
Subclasses may override in case the original URL (not rewritten, e.g.from
a web server rewrite module) needs to be returned instead of the URL
seen by QGIS server.
.. seealso:: :py:func:`originalUrl`
server, by default the two are equal
%End
QgsServerRequest::Method method() const;
@ -139,6 +138,16 @@ is available.
void setUrl( const QUrl &url );
%Docstring
Set the request url
%End
QUrl originalUrl() const;
%Docstring
Returns the request url as seen by the web server,
by default this is equal to the url seen by QGIS server
.. seealso:: :py:func:`url`
.. versionadded:: 3.6
%End
void setMethod( QgsServerRequest::Method method );
@ -146,6 +155,18 @@ Set the request url
Set the request method
%End
protected:
void setOriginalUrl( const QUrl &url );
%Docstring
Set the request original \url (the request url as seen by the web server)
.. seealso:: :py:func:`setUrl`
.. versionadded:: 3.6
%End
};
/************************************************************************

View File

@ -68,7 +68,7 @@ QgsFcgiServerRequest::QgsFcgiServerRequest()
}
// Store the URL before the server rewrite that could have been set in QUERY_STRING
mOriginalUrl = url;
setOriginalUrl( url );
// OGC parameters are passed with the query string, which is normally part of
// the REQUEST_URI, we override the query string url in case it is defined
@ -130,11 +130,6 @@ QByteArray QgsFcgiServerRequest::data() const
return mData;
}
QUrl QgsFcgiServerRequest::url() const
{
return mOriginalUrl.isEmpty() ? QgsServerRequest::url() : mOriginalUrl;
}
// Read post put data
void QgsFcgiServerRequest::readData()
{
@ -142,14 +137,22 @@ void QgsFcgiServerRequest::readData()
const char *lengthstr = getenv( "CONTENT_LENGTH" );
if ( lengthstr )
{
#ifdef QGISDEBUG
qDebug() << "fcgi: reading " << lengthstr << " bytes from stdin";
#endif
bool success = false;
int length = QString( lengthstr ).toInt( &success );
#ifdef QGISDEBUG
const char *request_body = getenv( "REQUEST_BODY" );
if ( success && request_body )
{
QString body( request_body );
body.truncate( length );
mData.append( body.toUtf8() );
length = 0;
}
qDebug() << "fcgi: reading " << lengthstr << " bytes from " << ( request_body ? "REQUEST_BODY" : "stdin" );
#endif
if ( success )
{
// XXX This not efficiont at all !!
// XXX This not efficient at all !!
for ( int i = 0; i < length; ++i )
{
mData.append( getchar() );

View File

@ -41,18 +41,6 @@ class SERVER_EXPORT QgsFcgiServerRequest: public QgsServerRequest
*/
bool hasError() const { return mHasError; }
/**
* \returns the request url
*
* Overrides base implementation because FCGI is typically behind
* a proxy server and QGIS Server will see a rewritten QUERY_STRING.
* FCGI implementation stores the REQUEST_URI (which is the URL seen
* by the proxy before it gets rewritten) and returns it instead of
* the rewritten one.
*/
QUrl url() const override;
private:
void readData();

View File

@ -27,6 +27,7 @@ QgsServerRequest::QgsServerRequest( const QString &url, Method method, const Hea
QgsServerRequest::QgsServerRequest( const QUrl &url, Method method, const Headers &headers )
: mUrl( url )
, mOriginalUrl( url )
, mMethod( method )
, mHeaders( headers )
{
@ -60,6 +61,16 @@ QUrl QgsServerRequest::url() const
return mUrl;
}
QUrl QgsServerRequest::originalUrl() const
{
return mOriginalUrl;
}
void QgsServerRequest::setOriginalUrl( const QUrl &url )
{
mOriginalUrl = url;
}
QgsServerRequest::Method QgsServerRequest::method() const
{
return mMethod;

View File

@ -82,13 +82,12 @@ class SERVER_EXPORT QgsServerRequest
virtual ~QgsServerRequest() = default;
/**
* \returns the request url
* \returns the request url as seen by QGIS server
*
* Subclasses may override in case the original URL (not rewritten, e.g.from
* a web server rewrite module) needs to be returned instead of the URL
* seen by QGIS server.
* \see originalUrl for the unrewritten url as seen by the web
* server, by default the two are equal
*/
virtual QUrl url() const;
QUrl url() const;
/**
* \returns the request method
@ -159,13 +158,36 @@ class SERVER_EXPORT QgsServerRequest
*/
void setUrl( const QUrl &url );
/**
* Returns the request url as seen by the web server,
* by default this is equal to the url seen by QGIS server
*
* \see url() for the rewritten url
* \since QGIS 3.6
*/
QUrl originalUrl() const;
/**
* Set the request method
*/
void setMethod( QgsServerRequest::Method method );
protected:
/**
* Set the request original \url (the request url as seen by the web server)
*
* \see setUrl() for the rewritten url
* \since QGIS 3.6
*/
void setOriginalUrl( const QUrl &url );
private:
// Url as seen by QGIS server after web server rewrite
QUrl mUrl;
// Unrewritten url as seen by the web server
QUrl mOriginalUrl;
Method mMethod = GetMethod;
// We mark as mutable in order
// to support lazy initialization

View File

@ -250,7 +250,7 @@ namespace QgsWcs
// Build default url
if ( href.isEmpty() )
{
QUrl url = request.url();
QUrl url = request.originalUrl();
QUrlQuery q( url );
for ( auto param : q.queryItems() )

View File

@ -36,28 +36,36 @@ namespace QgsWfs
QString serviceUrl( const QgsServerRequest &request, const QgsProject *project )
{
QString href;
QUrl href;
if ( project )
{
href = QgsServerProjectUtils::wfsServiceUrl( *project );
href.setUrl( QgsServerProjectUtils::wfsServiceUrl( *project ) );
}
// Build default url
if ( href.isEmpty() )
{
QUrl url = request.url();
QgsWfsParameters params;
params.load( QUrlQuery( url ) );
params.remove( QgsServerParameter::REQUEST );
params.remove( QgsServerParameter::VERSION_SERVICE );
params.remove( QgsServerParameter::SERVICE );
static QSet<QString> sFilter
{
QStringLiteral( "REQUEST" ),
QStringLiteral( "VERSION" ),
QStringLiteral( "SERVICE" ),
};
url.setQuery( params.urlQuery() );
href = url.toString();
href = request.originalUrl();
QUrlQuery q( href );
for ( auto param : q.queryItems() )
{
if ( sFilter.contains( param.first.toUpper() ) )
q.removeAllQueryItems( param.first );
}
href.setQuery( q );
}
return href;
return href.toString();
}
QString layerTypeName( const QgsMapLayer *layer )

View File

@ -56,7 +56,7 @@ namespace QgsWms
QStringLiteral( "_DC" )
};
href = request.url();
href = request.originalUrl();
QUrlQuery q( href );
for ( auto param : q.queryItems() )

View File

@ -59,7 +59,7 @@ namespace QgsWmts
// Build default url
if ( href.isEmpty() )
{
QUrl url = request.url();
QUrl url = request.originalUrl();
QgsWmtsParameters params;
params.load( QUrlQuery( url ) );

View File

@ -9,9 +9,6 @@ the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
"""
import unittest
import os
from urllib.parse import parse_qs, urlparse
__author__ = 'Alessandro Pasotti'
__date__ = '29/04/2017'
@ -20,16 +17,38 @@ __copyright__ = 'Copyright 2017, The QGIS Project'
__revision__ = '$Format:%H$'
import os
import unittest
from urllib.parse import parse_qs, urlencode, urlparse
from lxml import etree as ET
from qgis.PyQt.QtCore import QUrl
from qgis.server import QgsServerRequest, QgsFcgiServerRequest
from qgis.server import (QgsBufferServerResponse, QgsFcgiServerRequest,
QgsServerRequest)
from test_qgsserver import QgsServerTestBase
class QgsServerRequestTest(unittest.TestCase):
class QgsServerRequestTest(QgsServerTestBase):
@staticmethod
def _set_env(env={}):
for k in ('QUERY_STRING', 'REQUEST_URI', 'SERVER_NAME', 'CONTENT_LENGTH', 'SERVER_PORT', 'SCRIPT_NAME', 'REQUEST_BODY', 'REQUEST_METHOD'):
try:
del os.environ[k]
except KeyError:
pass
try:
os.environ[k] = env[k]
except KeyError:
pass
def test_requestHeaders(self):
"""Test request headers"""
headers = {'header-key-1': 'header-value-1', 'header-key-2': 'header-value-2'}
request = QgsServerRequest('http://somesite.com/somepath', QgsServerRequest.GetMethod, headers)
headers = {'header-key-1': 'header-value-1',
'header-key-2': 'header-value-2'}
request = QgsServerRequest(
'http://somesite.com/somepath', QgsServerRequest.GetMethod, headers)
for k, v in request.headers().items():
self.assertEqual(headers[k], v)
request.removeHeader('header-key-1')
@ -40,7 +59,8 @@ class QgsServerRequestTest(unittest.TestCase):
def test_requestParameters(self):
"""Test request parameters"""
request = QgsServerRequest('http://somesite.com/somepath?parm1=val1&parm2=val2', QgsServerRequest.GetMethod)
request = QgsServerRequest(
'http://somesite.com/somepath?parm1=val1&parm2=val2', QgsServerRequest.GetMethod)
parameters = {'PARM1': 'val1', 'PARM2': 'val2'}
for k, v in request.parameters().items():
self.assertEqual(parameters[k], v)
@ -52,18 +72,23 @@ class QgsServerRequestTest(unittest.TestCase):
def test_requestParametersDecoding(self):
"""Test request parameters decoding"""
request = QgsServerRequest('http://somesite.com/somepath?parm1=val1%20%2B+val2', QgsServerRequest.GetMethod)
request = QgsServerRequest(
'http://somesite.com/somepath?parm1=val1%20%2B+val2', QgsServerRequest.GetMethod)
self.assertEqual(request.parameters()['PARM1'], 'val1 + val2')
def test_requestUrl(self):
"""Test url"""
request = QgsServerRequest('http://somesite.com/somepath', QgsServerRequest.GetMethod)
self.assertEqual(request.url().toString(), 'http://somesite.com/somepath')
request = QgsServerRequest(
'http://somesite.com/somepath', QgsServerRequest.GetMethod)
self.assertEqual(request.url().toString(),
'http://somesite.com/somepath')
request.setUrl(QUrl('http://someother.com/someotherpath'))
self.assertEqual(request.url().toString(), 'http://someother.com/someotherpath')
self.assertEqual(request.url().toString(),
'http://someother.com/someotherpath')
def test_requestMethod(self):
request = QgsServerRequest('http://somesite.com/somepath', QgsServerRequest.GetMethod)
request = QgsServerRequest(
'http://somesite.com/somepath', QgsServerRequest.GetMethod)
self.assertEqual(request.method(), QgsServerRequest.GetMethod)
request.setMethod(QgsServerRequest.PostMethod)
self.assertEqual(request.method(), QgsServerRequest.PostMethod)
@ -71,43 +96,114 @@ class QgsServerRequestTest(unittest.TestCase):
def test_fcgiRequest(self):
"""Test various combinations of FCGI env parameters with rewritten urls"""
def _test_url(url, env={}):
for k in ('QUERY_STRING', 'REQUEST_URI', 'SERVER_NAME', 'SERVER_PORT', 'SCRIPT_NAME'):
try:
del os.environ[k]
except KeyError:
pass
try:
os.environ[k] = env[k]
except KeyError:
pass
def _test_url(original_url, rewritten_url, env={}):
self._set_env(env)
request = QgsFcgiServerRequest()
self.assertEqual(request.url().toString(), url)
self.assertEqual(request.originalUrl().toString(), original_url)
self.assertEqual(request.url().toString(), rewritten_url)
# Check MAP
if 'QUERY_STRING' in env:
map = {k.upper(): v[0] for k, v in parse_qs(env['QUERY_STRING']).items()}['MAP']
map = {k.upper(): v[0] for k, v in parse_qs(
env['QUERY_STRING']).items()}['MAP']
else:
map = {k.upper(): v[0] for k, v in parse_qs(urlparse(env['REQUEST_URI']).query).items()}['MAP']
map = {k.upper(): v[0] for k, v in parse_qs(
urlparse(env['REQUEST_URI']).query).items()}['MAP']
self.assertEqual(request.parameter('MAP'), map)
_test_url('http://somesite.com/somepath/index.html?map=/my/path.qgs', {
'REQUEST_URI': '/somepath/index.html?map=/my/path.qgs',
'SERVER_NAME': 'somesite.com',
})
_test_url('http://somesite.com/somepath?map=/my/path.qgs', {
'REQUEST_URI': '/somepath?map=/my/path.qgs',
'SERVER_NAME': 'somesite.com',
})
_test_url('http://somesite.com/somepath/path', {
'REQUEST_URI': '/somepath/path',
'SERVER_NAME': 'somesite.com',
'QUERY_STRING': 'map=/my/path.qgs'
})
_test_url('http://somesite.com/somepath/path/?token=QGIS2019', {
'REQUEST_URI': '/somepath/path/?token=QGIS2019',
'SERVER_NAME': 'somesite.com',
'QUERY_STRING': 'map=/my/path.qgs',
})
_test_url('http://somesite.com/somepath/project1/',
'http://somesite.com/somepath/project1/?map=/my/project1.qgs', {
'REQUEST_URI': '/somepath/project1/',
'SERVER_NAME': 'somesite.com',
'QUERY_STRING': 'map=/my/project1.qgs'
})
_test_url('http://somesite.com/somepath/path/?token=QGIS2019',
'http://somesite.com/somepath/path/?map=/my/path.qgs', {
'REQUEST_URI': '/somepath/path/?token=QGIS2019',
'SERVER_NAME': 'somesite.com',
'QUERY_STRING': 'map=/my/path.qgs',
})
_test_url('http://somesite.com/somepath/index.html?map=/my/path.qgs',
'http://somesite.com/somepath/index.html?map=/my/path.qgs',
{
'REQUEST_URI': '/somepath/index.html?map=/my/path.qgs',
'SERVER_NAME': 'somesite.com',
})
_test_url('http://somesite.com/somepath?map=/my/path.qgs',
'http://somesite.com/somepath?map=/my/path.qgs',
{
'REQUEST_URI': '/somepath?map=/my/path.qgs',
'SERVER_NAME': 'somesite.com',
})
def test_fcgiRequestPOST(self):
"""Test various combinations of FCGI POST parameters with rewritten urls"""
def _check_links(params, method='GET'):
data = urlencode(params)
if method == 'GET':
env = {
'SERVER_NAME': 'www.myserver.com',
'REQUEST_URI': '/aproject/',
'QUERY_STRING': data,
'REQUEST_METHOD': 'GET',
}
else:
env = {
'SERVER_NAME': 'www.myserver.com',
'REQUEST_URI': '/aproject/',
'REQUEST_BODY': data,
'CONTENT_LENGTH': str(len(data)),
'REQUEST_METHOD': 'POST',
}
self._set_env(env)
request = QgsFcgiServerRequest()
response = QgsBufferServerResponse()
self.server.handleRequest(request, response)
self.assertFalse(b'ServiceExceptionReport' in response.body())
if method == 'POST':
self.assertEqual(request.data(), data.encode('utf8'))
else:
original_url = request.originalUrl().toString()
self.assertTrue(original_url.startswith('http://www.myserver.com/aproject/'))
self.assertEqual(original_url.find(urlencode({'MAP': params['map']})), -1)
e = ET.fromstring(bytes(response.body()))
elems = []
for ns in ('wms', 'wfs', 'wcs'):
elems += e.xpath('//xxx:OnlineResource', namespaces={'xxx': 'http://www.opengis.net/%s' % ns})
elems += e.xpath('//ows:Get/ows:OnlineResource', namespaces={'ows': 'http://www.opengis.net/ows'})
elems += e.xpath('//ows:Post', namespaces={'ows': 'http://www.opengis.net/ows'})
elems += e.xpath('//ows:Get', namespaces={'ows': 'http://www.opengis.net/ows'})
elems += e.xpath('//ows:Get', namespaces={'ows': 'http://www.opengis.net/ows/1.1'})
self.assertTrue(len(elems) > 0)
for _e in elems:
attrs = _e.attrib
href = attrs['{http://www.w3.org/1999/xlink}href']
self.assertTrue(href.startswith('http://www.myserver.com/aproject/'))
self.assertEqual(href.find(urlencode({'MAP': params['map']})), -1)
# Test post request handler
params = {
'map': os.path.join(self.testdata_path, 'test_project_wfs.qgs'),
'REQUEST': 'GetCapabilities',
'SERVICE': 'WFS',
}
_check_links(params)
_check_links(params, 'POST')
params['SERVICE'] = 'WMS'
_check_links(params)
_check_links(params, 'POST')
params['SERVICE'] = 'WCS'
_check_links(params)
_check_links(params, 'POST')
params['SERVICE'] = 'WMTS'
_check_links(params)
_check_links(params, 'POST')
if __name__ == '__main__':

View File

@ -4,10 +4,10 @@
Tests for WFS-T provider using QGIS Server through qgis_wrapped_server.py.
This is an integration test for QGIS Desktop WFS-T provider and QGIS Server
WFS-T that check if QGIS can talk to and uderstand itself.
WFS-T that check if QGIS can talk to and understand itself.
The test uses testdata/wfs_transactional/wfs_transactional.qgs and three
initially empty shapefiles layrs with points, lines and polygons.
initially empty shapefiles layers with points, lines and polygons.
All WFS-T calls are executed through the QGIS WFS data provider.