Skip to content

Commit 6fd3f74

Browse files
committedNov 5, 2016
[tests] Authmanager tests for username/pwd and PKI
1 parent ecef7d7 commit 6fd3f74

11 files changed

+973
-111
lines changed
 

‎tests/src/python/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,7 @@ ENDIF (WITH_APIDOC)
114114
IF (WITH_SERVER)
115115
ADD_PYTHON_TEST(PyQgsServer test_qgsserver.py)
116116
ADD_PYTHON_TEST(PyQgsServerAccessControl test_qgsserver_accesscontrol.py)
117+
ADD_PYTHON_TEST(PyQgsAuthManagerPasswordOWSTest test_authmanager_password_ows.py)
118+
#ADD_PYTHON_TEST(PyQgsAuthManagerPKIOWSTest test_authmanager_pki_ows.py)
119+
ADD_PYTHON_TEST(PyQgsAuthManagerPKIPostgresTest test_authmanager_pki_postgres.py)
117120
ENDIF (WITH_SERVER)
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
QGIS Server HTTP wrapper
4+
5+
This script launches a QGIS Server listening on port 8081 or on the port
6+
specified on the environment variable QGIS_SERVER_PORT.
7+
QGIS_SERVER_HOST (defaults to 127.0.0.1)
8+
9+
For testing purposes, HTTP Basic can be enabled by setting the following
10+
environment variables:
11+
12+
* QGIS_SERVER_HTTP_BASIC_AUTH (default not set, set to anything to enable)
13+
* QGIS_SERVER_USERNAME (default ="username")
14+
* QGIS_SERVER_PASSWORD (default ="password")
15+
16+
PKI authentication with HTTPS can be enabled with:
17+
18+
* QGIS_SERVER_PKI_CERTIFICATE (server certificate)
19+
* QGIS_SERVER_PKI_KEY (server private key)
20+
* QGIS_SERVER_PKI_AUTHORITY (root CA)
21+
* QGIS_SERVER_PKI_USERNAME (valid username)
22+
23+
Sample run:
24+
25+
QGIS_SERVER_PKI_USERNAME=Gerardus QGIS_SERVER_PORT=47547 QGIS_SERVER_HOST=localhost \
26+
QGIS_SERVER_PKI_KEY=/home/dev/QGIS/tests/testdata/auth_system/certs_keys/localhost_ssl_key.pem \
27+
QGIS_SERVER_PKI_CERTIFICATE=/home/dev/QGIS/tests/testdata/auth_system/certs_keys/localhost_ssl_cert.pem \
28+
QGIS_SERVER_PKI_AUTHORITY=/home/dev/QGIS/tests/testdata/auth_system/certs_keys/chains_subissuer-issuer-root_issuer2-root2.pem \
29+
python /home/dev/QGIS/tests/src/python/qgis_wrapped_server.py
30+
31+
.. note:: This program is free software; you can redistribute it and/or modify
32+
it under the terms of the GNU General Public License as published by
33+
the Free Software Foundation; either version 2 of the License, or
34+
(at your option) any later version.
35+
"""
36+
from __future__ import print_function
37+
from future import standard_library
38+
standard_library.install_aliases()
39+
40+
__author__ = 'Alessandro Pasotti'
41+
__date__ = '05/15/2016'
42+
__copyright__ = 'Copyright 2016, The QGIS Project'
43+
# This will get replaced with a git SHA1 when you do a git archive
44+
__revision__ = '$Format:%H$'
45+
46+
47+
import os
48+
import sys
49+
import ssl
50+
import urllib.parse
51+
from http.server import BaseHTTPRequestHandler, HTTPServer
52+
from qgis.server import QgsServer, QgsServerFilter
53+
54+
QGIS_SERVER_PORT = int(os.environ.get('QGIS_SERVER_PORT', '8081'))
55+
QGIS_SERVER_HOST = os.environ.get('QGIS_SERVER_HOST', '127.0.0.1')
56+
# PKI authentication
57+
QGIS_SERVER_PKI_CERTIFICATE = os.environ.get('QGIS_SERVER_PKI_CERTIFICATE')
58+
QGIS_SERVER_PKI_KEY = os.environ.get('QGIS_SERVER_PKI_KEY')
59+
QGIS_SERVER_PKI_AUTHORITY = os.environ.get('QGIS_SERVER_PKI_AUTHORITY')
60+
QGIS_SERVER_PKI_USERNAME = os.environ.get('QGIS_SERVER_PKI_USERNAME')
61+
62+
# Check if PKI - https is enabled
63+
https = (QGIS_SERVER_PKI_CERTIFICATE is not None and
64+
os.path.isfile(QGIS_SERVER_PKI_CERTIFICATE) and
65+
QGIS_SERVER_PKI_KEY is not None and
66+
os.path.isfile(QGIS_SERVER_PKI_KEY) and
67+
QGIS_SERVER_PKI_AUTHORITY is not None and
68+
os.path.isfile(QGIS_SERVER_PKI_AUTHORITY) and
69+
QGIS_SERVER_PKI_USERNAME)
70+
71+
qgs_server = QgsServer()
72+
73+
if os.environ.get('QGIS_SERVER_HTTP_BASIC_AUTH') is not None:
74+
import base64
75+
76+
class HTTPBasicFilter(QgsServerFilter):
77+
78+
def responseComplete(self):
79+
request = self.serverInterface().requestHandler()
80+
if self.serverInterface().getEnv('HTTP_AUTHORIZATION'):
81+
username, password = base64.b64decode(self.serverInterface().getEnv('HTTP_AUTHORIZATION')[6:]).split(':')
82+
if (username == os.environ.get('QGIS_SERVER_USERNAME', 'username')
83+
and password == os.environ.get('QGIS_SERVER_PASSWORD', 'password')):
84+
return
85+
# No auth ...
86+
request.clearHeaders()
87+
request.setHeader('Status', '401 Authorization required')
88+
request.setHeader('WWW-Authenticate', 'Basic realm="QGIS Server"')
89+
request.clearBody()
90+
request.appendBody('<h1>Authorization required</h1>')
91+
92+
filter = HTTPBasicFilter(qgs_server.serverInterface())
93+
qgs_server.serverInterface().registerFilter(filter)
94+
95+
96+
class Handler(BaseHTTPRequestHandler):
97+
98+
def do_GET(self):
99+
# For PKI: check the username from client certificate
100+
if https:
101+
try:
102+
ssl.match_hostname(self.connection.getpeercert(), QGIS_SERVER_PKI_USERNAME)
103+
except Exception as ex:
104+
print("SSL Exception %s" % ex)
105+
self.send_response(401)
106+
self.end_headers()
107+
self.wfile.write('UNAUTHORIZED')
108+
return
109+
# CGI vars:
110+
for k, v in self.headers.items():
111+
# Uncomment to print debug info about env vars passed into QGIS Server env
112+
#print('Setting ENV var %s to %s' % ('HTTP_%s' % k.replace(' ', '-').replace('-', '_').replace(' ', '-').upper(), v))
113+
qgs_server.putenv('HTTP_%s' % k.replace(' ', '-').replace('-', '_').replace(' ', '-').upper(), v)
114+
qgs_server.putenv('SERVER_PORT', str(self.server.server_port))
115+
qgs_server.putenv('SERVER_NAME', self.server.server_name)
116+
qgs_server.putenv('REQUEST_URI', self.path)
117+
parsed_path = urllib.parse.urlparse(self.path)
118+
headers, body = qgs_server.handleRequest(parsed_path.query)
119+
headers_dict = dict(h.split(': ', 1) for h in headers.decode().split('\n') if h)
120+
try:
121+
self.send_response(int(headers_dict['Status'].split(' ')[0]))
122+
except:
123+
self.send_response(200)
124+
for k, v in headers_dict.items():
125+
self.send_header(k, v)
126+
self.end_headers()
127+
self.wfile.write(body)
128+
return
129+
130+
def do_POST(self):
131+
content_len = int(self.headers.get('content-length', 0))
132+
post_body = self.rfile.read(content_len).decode()
133+
request = post_body[1:post_body.find(' ')]
134+
self.path = self.path + '&REQUEST_BODY=' + \
135+
post_body.replace('&amp;', '') + '&REQUEST=' + request
136+
return self.do_GET()
137+
138+
139+
if __name__ == '__main__':
140+
server = HTTPServer((QGIS_SERVER_HOST, QGIS_SERVER_PORT), Handler)
141+
if https:
142+
server.socket = ssl.wrap_socket(server.socket,
143+
certfile=QGIS_SERVER_PKI_CERTIFICATE,
144+
keyfile=QGIS_SERVER_PKI_KEY,
145+
ca_certs=QGIS_SERVER_PKI_AUTHORITY,
146+
cert_reqs=ssl.CERT_REQUIRED,
147+
server_side=True,
148+
ssl_version=ssl.PROTOCOL_TLSv1)
149+
message = 'Starting server on %s://%s:%s, use <Ctrl-C> to stop' % \
150+
('https' if https else 'http', QGIS_SERVER_HOST, server.server_port)
151+
try:
152+
print(message, flush=True)
153+
except:
154+
print(message)
155+
sys.stdout.flush()
156+
server.serve_forever()
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Tests for auth manager WMS/WFS using QGIS Server through HTTP Basic
4+
enabled qgis_wrapped_server.py.
5+
6+
This is an integration test for QGIS Desktop Auth Manager WFS and WMS provider
7+
and QGIS Server WFS/WMS that check if QGIS can use a stored auth manager auth
8+
configuration to access an HTTP Basic protected endpoint.
9+
10+
11+
From build dir, run: ctest -R PyQgsAuthManagerPasswordOWSTest -V
12+
13+
.. note:: This program is free software; you can redistribute it and/or modify
14+
it under the terms of the GNU General Public License as published by
15+
the Free Software Foundation; either version 2 of the License, or
16+
(at your option) any later version.
17+
"""
18+
import os
19+
import sys
20+
import re
21+
import subprocess
22+
import tempfile
23+
import random
24+
import string
25+
try:
26+
from urllib.parse import quote
27+
except:
28+
from urllib import quote
29+
30+
__author__ = 'Alessandro Pasotti'
31+
__date__ = '18/09/2016'
32+
__copyright__ = 'Copyright 2016, The QGIS Project'
33+
# This will get replaced with a git SHA1 when you do a git archive
34+
__revision__ = '$Format:%H$'
35+
36+
from shutil import rmtree
37+
38+
from utilities import unitTestDataPath, waitServer
39+
from qgis.core import (
40+
QgsAuthManager,
41+
QgsAuthMethodConfig,
42+
QgsVectorLayer,
43+
QgsRasterLayer,
44+
)
45+
from qgis.testing import (
46+
start_app,
47+
unittest,
48+
)
49+
50+
try:
51+
QGIS_SERVER_ENDPOINT_PORT = os.environ['QGIS_SERVER_ENDPOINT_PORT']
52+
except:
53+
QGIS_SERVER_ENDPOINT_PORT = '0' # Auto
54+
55+
56+
QGIS_AUTH_DB_DIR_PATH = tempfile.mkdtemp()
57+
58+
os.environ['QGIS_AUTH_DB_DIR_PATH'] = QGIS_AUTH_DB_DIR_PATH
59+
60+
qgis_app = start_app()
61+
62+
63+
class TestAuthManager(unittest.TestCase):
64+
65+
@classmethod
66+
def setUpClass(cls):
67+
"""Run before all tests:
68+
Creates an auth configuration"""
69+
cls.port = QGIS_SERVER_ENDPOINT_PORT
70+
# Clean env just to be sure
71+
env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE']
72+
for ev in env_vars:
73+
try:
74+
del os.environ[ev]
75+
except KeyError:
76+
pass
77+
cls.testdata_path = unitTestDataPath('qgis_server') + '/'
78+
cls.project_path = cls.testdata_path + "test_project.qgs"
79+
# Enable auth
80+
#os.environ['QGIS_AUTH_PASSWORD_FILE'] = QGIS_AUTH_PASSWORD_FILE
81+
authm = QgsAuthManager.instance()
82+
assert (authm.setMasterPassword('masterpassword', True))
83+
cls.auth_config = QgsAuthMethodConfig('Basic')
84+
cls.auth_config.setName('test_auth_config')
85+
cls.username = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(6))
86+
cls.password = cls.username[::-1] # reversed
87+
cls.auth_config.setConfig('username', cls.username)
88+
cls.auth_config.setConfig('password', cls.password)
89+
assert (authm.storeAuthenticationConfig(cls.auth_config)[0])
90+
cls.hostname = '127.0.0.1'
91+
cls.protocol = 'http'
92+
93+
os.environ['QGIS_SERVER_HTTP_BASIC_AUTH'] = '1'
94+
os.environ['QGIS_SERVER_USERNAME'] = cls.username
95+
os.environ['QGIS_SERVER_PASSWORD'] = cls.password
96+
os.environ['QGIS_SERVER_PORT'] = str(cls.port)
97+
os.environ['QGIS_SERVER_HOST'] = cls.hostname
98+
server_path = os.path.dirname(os.path.realpath(__file__)) + \
99+
'/qgis_wrapped_server.py'
100+
cls.server = subprocess.Popen([sys.executable, server_path],
101+
env=os.environ, stdout=subprocess.PIPE)
102+
103+
line = cls.server.stdout.readline()
104+
cls.port = int(re.findall(b':(\d+)', line)[0])
105+
assert cls.port != 0
106+
# Wait for the server process to start
107+
assert waitServer('%s://%s:%s' % (cls.protocol, cls.hostname, cls.port)), "Server is not responding! '%s://%s:%s" % (cls.protocol, cls.hostname, cls.port)
108+
109+
@classmethod
110+
def tearDownClass(cls):
111+
"""Run after all tests"""
112+
cls.server.terminate()
113+
rmtree(QGIS_AUTH_DB_DIR_PATH)
114+
del cls.server
115+
116+
def setUp(self):
117+
"""Run before each test."""
118+
pass
119+
120+
def tearDown(self):
121+
"""Run after each test."""
122+
pass
123+
124+
@classmethod
125+
def _getWFSLayer(cls, type_name, layer_name=None, authcfg=None):
126+
"""
127+
WFS layer factory
128+
"""
129+
if layer_name is None:
130+
layer_name = 'wfs_' + type_name
131+
parms = {
132+
'srsname': 'EPSG:4326',
133+
'typename': type_name.decode('utf-8').replace(' ', '_'),
134+
'url': '%s://%s:%s/?map=%s' % (cls.protocol, cls.hostname, cls.port, cls.project_path),
135+
'version': '1.0.0',
136+
'table': '',
137+
}
138+
uri = u'%(url)s&SERVICE=WFS&VERSION=%(version)s&REQUEST=GetFeature&TYPENAME=%(typename)s&SRSNAME=%(srsname)s' % parms
139+
if authcfg is not None:
140+
uri = uri + "&authcfg=%s" % authcfg
141+
wfs_layer = QgsVectorLayer(uri, layer_name, 'WFS')
142+
return wfs_layer
143+
144+
@classmethod
145+
def _getWMSLayer(cls, layers, layer_name=None, authcfg=None):
146+
"""
147+
WMS layer factory
148+
"""
149+
if layer_name is None:
150+
layer_name = 'wms_' + layers.replace(',', '')
151+
parms = {
152+
'crs': 'EPSG:4326',
153+
'url': '%s://%s:%s/?map=%s' % (cls.protocol, cls.hostname, cls.port, cls.project_path),
154+
'format': 'image/png',
155+
'layers': quote(layers),
156+
'styles': '',
157+
'version': 'auto',
158+
#'sql': '',
159+
}
160+
if authcfg is not None:
161+
parms.update({'authcfg': authcfg})
162+
uri = '&'.join([("%s=%s" % (k, v.replace('=', '%3D'))) for k, v in list(parms.items())])
163+
wms_layer = QgsRasterLayer(uri, layer_name, 'wms')
164+
return wms_layer
165+
166+
def testValidAuthAccess(self):
167+
"""
168+
Access the HTTP Basic protected layer with valid credentials
169+
"""
170+
wfs_layer = self._getWFSLayer('testlayer èé', authcfg=self.auth_config.id())
171+
self.assertTrue(wfs_layer.isValid())
172+
wms_layer = self._getWMSLayer('testlayer èé', authcfg=self.auth_config.id())
173+
self.assertTrue(wms_layer.isValid())
174+
175+
def testInvalidAuthAccess(self):
176+
"""
177+
Access the HTTP Basic protected layer with no credentials
178+
"""
179+
wfs_layer = self._getWFSLayer('testlayer èé')
180+
self.assertFalse(wfs_layer.isValid())
181+
wms_layer = self._getWMSLayer('testlayer èé')
182+
self.assertFalse(wms_layer.isValid())
183+
184+
185+
if __name__ == '__main__':
186+
unittest.main()
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Tests for auth manager WMS/WFS using QGIS Server through PKI
4+
enabled qgis_wrapped_server.py.
5+
6+
This is an integration test for QGIS Desktop Auth Manager WFS and WMS provider
7+
and QGIS Server WFS/WMS that check if QGIS can use a stored auth manager auth
8+
configuration to access an HTTP Basic protected endpoint.
9+
10+
11+
From build dir, run: ctest -R PyQgsAuthManagerPKIOWSTest -V
12+
13+
.. note:: This program is free software; you can redistribute it and/or modify
14+
it under the terms of the GNU General Public License as published by
15+
the Free Software Foundation; either version 2 of the License, or
16+
(at your option) any later version.
17+
"""
18+
import os
19+
import sys
20+
import re
21+
import subprocess
22+
import tempfile
23+
try:
24+
from urllib.parse import quote
25+
except:
26+
from urllib import quote
27+
import stat
28+
29+
__author__ = 'Alessandro Pasotti'
30+
__date__ = '25/10/2016'
31+
__copyright__ = 'Copyright 2016, The QGIS Project'
32+
# This will get replaced with a git SHA1 when you do a git archive
33+
__revision__ = '$Format:%H$'
34+
35+
from shutil import rmtree
36+
37+
from utilities import unitTestDataPath, waitServer
38+
from qgis.core import (
39+
QgsAuthManager,
40+
QgsAuthMethodConfig,
41+
QgsVectorLayer,
42+
QgsRasterLayer,
43+
)
44+
45+
from qgis.PyQt.QtNetwork import QSslCertificate
46+
47+
from qgis.testing import (
48+
start_app,
49+
unittest,
50+
)
51+
52+
try:
53+
QGIS_SERVER_ENDPOINT_PORT = os.environ['QGIS_SERVER_ENDPOINT_PORT']
54+
except:
55+
QGIS_SERVER_ENDPOINT_PORT = '0' # Auto
56+
57+
58+
QGIS_AUTH_DB_DIR_PATH = tempfile.mkdtemp()
59+
60+
os.environ['QGIS_AUTH_DB_DIR_PATH'] = QGIS_AUTH_DB_DIR_PATH
61+
62+
qgis_app = start_app()
63+
64+
65+
class TestAuthManager(unittest.TestCase):
66+
67+
@classmethod
68+
def setUpAuth(cls):
69+
"""Run before all tests and set up authentication"""
70+
authm = QgsAuthManager.instance()
71+
assert (authm.setMasterPassword('masterpassword', True))
72+
cls.sslrootcert_path = os.path.join(cls.certsdata_path, 'chains_subissuer-issuer-root_issuer2-root2.pem')
73+
cls.sslcert = os.path.join(cls.certsdata_path, 'gerardus_cert.pem')
74+
cls.sslkey = os.path.join(cls.certsdata_path, 'gerardus_key.pem')
75+
assert os.path.isfile(cls.sslcert)
76+
assert os.path.isfile(cls.sslkey)
77+
assert os.path.isfile(cls.sslrootcert_path)
78+
os.chmod(cls.sslcert, stat.S_IRUSR)
79+
os.chmod(cls.sslkey, stat.S_IRUSR)
80+
os.chmod(cls.sslrootcert_path, stat.S_IRUSR)
81+
cls.auth_config = QgsAuthMethodConfig("PKI-Paths")
82+
cls.auth_config.setConfig('certpath', cls.sslcert)
83+
cls.auth_config.setConfig('keypath', cls.sslkey)
84+
cls.auth_config.setName('test_pki_auth_config')
85+
cls.username = 'Gerardus'
86+
cls.sslrootcert = QSslCertificate.fromPath(cls.sslrootcert_path)
87+
assert cls.sslrootcert is not None
88+
authm.storeCertAuthorities(cls.sslrootcert)
89+
authm.rebuildCaCertsCache()
90+
authm.rebuildTrustedCaCertsCache()
91+
assert (authm.storeAuthenticationConfig(cls.auth_config)[0])
92+
assert cls.auth_config.isValid()
93+
94+
cls.server_cert = os.path.join(cls.certsdata_path, 'localhost_ssl_cert.pem')
95+
cls.server_key = os.path.join(cls.certsdata_path, 'localhost_ssl_key.pem')
96+
cls.server_rootcert = cls.sslrootcert_path
97+
os.chmod(cls.server_cert, stat.S_IRUSR)
98+
os.chmod(cls.server_key, stat.S_IRUSR)
99+
os.chmod(cls.server_rootcert, stat.S_IRUSR)
100+
101+
os.environ['QGIS_SERVER_HOST'] = cls.hostname
102+
os.environ['QGIS_SERVER_PORT'] = str(cls.port)
103+
os.environ['QGIS_SERVER_PKI_KEY'] = cls.server_key
104+
os.environ['QGIS_SERVER_PKI_CERTIFICATE'] = cls.server_cert
105+
os.environ['QGIS_SERVER_PKI_USERNAME'] = cls.username
106+
os.environ['QGIS_SERVER_PKI_AUTHORITY'] = cls.server_rootcert
107+
108+
@classmethod
109+
def setUpClass(cls):
110+
"""Run before all tests:
111+
Creates an auth configuration"""
112+
cls.port = QGIS_SERVER_ENDPOINT_PORT
113+
# Clean env just to be sure
114+
env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE']
115+
for ev in env_vars:
116+
try:
117+
del os.environ[ev]
118+
except KeyError:
119+
pass
120+
cls.testdata_path = unitTestDataPath('qgis_server')
121+
cls.certsdata_path = os.path.join(unitTestDataPath('auth_system'), 'certs_keys')
122+
cls.project_path = os.path.join(cls.testdata_path, "test_project.qgs")
123+
cls.hostname = 'localhost'
124+
cls.protocol = 'https'
125+
126+
cls.setUpAuth()
127+
128+
server_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
129+
'qgis_wrapped_server.py')
130+
cls.server = subprocess.Popen([sys.executable, server_path],
131+
env=os.environ, stdout=subprocess.PIPE)
132+
line = cls.server.stdout.readline()
133+
cls.port = int(re.findall(b':(\d+)', line)[0])
134+
assert cls.port != 0
135+
# Wait for the server process to start
136+
assert waitServer('%s://%s:%s' % (cls.protocol, cls.hostname, cls.port)), "Server is not responding! %s://%s:%s" % (cls.protocol, cls.hostname, cls.port)
137+
138+
@classmethod
139+
def tearDownClass(cls):
140+
"""Run after all tests"""
141+
cls.server.terminate()
142+
rmtree(QGIS_AUTH_DB_DIR_PATH)
143+
del cls.server
144+
145+
def setUp(self):
146+
"""Run before each test."""
147+
pass
148+
149+
def tearDown(self):
150+
"""Run after each test."""
151+
pass
152+
153+
def _getWFSLayer(cls, type_name, layer_name=None, authcfg=None):
154+
"""
155+
WFS layer factory
156+
"""
157+
if layer_name is None:
158+
layer_name = 'wfs_' + type_name
159+
parms = {
160+
'srsname': 'EPSG:4326',
161+
'typename': type_name.decode('utf-8').replace(' ', '_'),
162+
'url': '%s://%s:%s/?map=%s' % (cls.protocol, cls.hostname, cls.port, cls.project_path),
163+
'version': '1.0.0',
164+
'table': '',
165+
}
166+
uri = u'%(url)s&SERVICE=WFS&VERSION=%(version)s&REQUEST=GetFeature&TYPENAME=%(typename)s&SRSNAME=%(srsname)s' % parms
167+
if authcfg is not None:
168+
uri = uri + "&authcfg=%s" % authcfg
169+
wfs_layer = QgsVectorLayer(uri, layer_name, 'WFS')
170+
return wfs_layer
171+
172+
@classmethod
173+
def _getWMSLayer(cls, layers, layer_name=None, authcfg=None):
174+
"""
175+
WMS layer factory
176+
"""
177+
if layer_name is None:
178+
layer_name = 'wms_' + layers.replace(',', '')
179+
parms = {
180+
'crs': 'EPSG:4326',
181+
'url': '%s://%s:%s/?map=%s' % (cls.protocol, cls.hostname, cls.port, cls.project_path),
182+
'format': 'image/png',
183+
'layers': quote(layers),
184+
'styles': '',
185+
'version': 'auto',
186+
#'sql': '',
187+
}
188+
if authcfg is not None:
189+
parms.update({'authcfg': authcfg})
190+
uri = '&'.join([("%s=%s" % (k, v.replace('=', '%3D'))) for k, v in list(parms.items())])
191+
wms_layer = QgsRasterLayer(uri, layer_name, 'wms')
192+
return wms_layer
193+
194+
def testValidAuthAccess(self):
195+
"""
196+
Access the protected layer with valid credentials
197+
Note: cannot test invalid access in a separate test because
198+
it would fail the subsequent (valid) calls due to cached connections
199+
"""
200+
wfs_layer = self._getWFSLayer('testlayer èé', authcfg=self.auth_config.id())
201+
self.assertTrue(wfs_layer.isValid())
202+
wms_layer = self._getWMSLayer('testlayer èé', authcfg=self.auth_config.id())
203+
self.assertTrue(wms_layer.isValid())
204+
205+
206+
if __name__ == '__main__':
207+
unittest.main()
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Tests for auth manager PKI access to postgres.
4+
5+
This is an integration test for QGIS Desktop Auth Manager postgres provider that
6+
checks if QGIS can use a stored auth manager auth configuration to access
7+
a PKI protected postgres.
8+
9+
Configuration form the environment:
10+
11+
* QGIS_POSTGRES_SERVER_PORT (default: 55432)
12+
* QGIS_POSTGRES_EXECUTABLE_PATH (default: /usr/lib/postgresql/9.4/bin)
13+
14+
15+
From build dir, run: ctest -R PyQgsAuthManagerPKIPostgresTest -V
16+
17+
.. note:: This program is free software; you can redistribute it and/or modify
18+
it under the terms of the GNU General Public License as published by
19+
the Free Software Foundation; either version 2 of the License, or
20+
(at your option) any later version.
21+
"""
22+
import os
23+
import time
24+
import signal
25+
import stat
26+
import subprocess
27+
import tempfile
28+
29+
from shutil import rmtree
30+
31+
from utilities import unitTestDataPath
32+
from qgis.core import (
33+
QgsAuthManager,
34+
QgsAuthMethodConfig,
35+
QgsVectorLayer,
36+
QgsDataSourceURI,
37+
QgsWKBTypes,
38+
)
39+
40+
41+
from qgis.PyQt.QtNetwork import QSslCertificate
42+
43+
from qgis.testing import (
44+
start_app,
45+
unittest,
46+
)
47+
48+
49+
__author__ = 'Alessandro Pasotti'
50+
__date__ = '25/10/2016'
51+
__copyright__ = 'Copyright 2016, The QGIS Project'
52+
# This will get replaced with a git SHA1 when you do a git archive
53+
__revision__ = '$Format:%H$'
54+
55+
QGIS_POSTGRES_SERVER_PORT = os.environ.get('QGIS_POSTGRES_SERVER_PORT', '55432')
56+
QGIS_POSTGRES_EXECUTABLE_PATH = os.environ.get('QGIS_POSTGRES_EXECUTABLE_PATH', '/usr/lib/postgresql/9.4/bin')
57+
58+
assert os.path.exists(QGIS_POSTGRES_EXECUTABLE_PATH)
59+
60+
QGIS_AUTH_DB_DIR_PATH = tempfile.mkdtemp()
61+
62+
# Postgres test path
63+
QGIS_PG_TEST_PATH = tempfile.mkdtemp()
64+
65+
os.environ['QGIS_AUTH_DB_DIR_PATH'] = QGIS_AUTH_DB_DIR_PATH
66+
67+
qgis_app = start_app()
68+
69+
QGIS_POSTGRES_CONF_TEMPLATE = """
70+
hba_file = '%(tempfolder)s/pg_hba.conf'
71+
listen_addresses = '*'
72+
port = %(port)s
73+
max_connections = 100
74+
unix_socket_directories = '%(tempfolder)s'
75+
ssl = true
76+
ssl_ciphers = 'DEFAULT:!LOW:!EXP:!MD5:@STRENGTH' # allowed SSL ciphers
77+
ssl_cert_file = '%(server_cert)s'
78+
ssl_key_file = '%(server_key)s'
79+
ssl_ca_file = '%(sslrootcert_path)s'
80+
password_encryption = on
81+
"""
82+
83+
QGIS_POSTGRES_HBA_TEMPLATE = """
84+
hostssl all all 0.0.0.0/0 cert clientcert=1
85+
hostssl all all ::1/0 cert clientcert=1
86+
host all all 127.0.0.1/32 trust
87+
host all all ::1/32 trust
88+
"""
89+
90+
91+
class TestAuthManager(unittest.TestCase):
92+
93+
@classmethod
94+
def setUpAuth(cls):
95+
"""Run before all tests and set up authentication"""
96+
authm = QgsAuthManager.instance()
97+
assert (authm.setMasterPassword('masterpassword', True))
98+
cls.pg_conf = os.path.join(cls.tempfolder, 'postgresql.conf')
99+
cls.pg_hba = os.path.join(cls.tempfolder, 'pg_hba.conf')
100+
# Client side
101+
cls.sslrootcert_path = os.path.join(cls.certsdata_path, 'chains_subissuer-issuer-root_issuer2-root2.pem')
102+
cls.sslcert = os.path.join(cls.certsdata_path, 'gerardus_cert.pem')
103+
cls.sslkey = os.path.join(cls.certsdata_path, 'gerardus_key.pem')
104+
assert os.path.isfile(cls.sslcert)
105+
assert os.path.isfile(cls.sslkey)
106+
assert os.path.isfile(cls.sslrootcert_path)
107+
os.chmod(cls.sslcert, stat.S_IRUSR)
108+
os.chmod(cls.sslkey, stat.S_IRUSR)
109+
os.chmod(cls.sslrootcert_path, stat.S_IRUSR)
110+
cls.auth_config = QgsAuthMethodConfig("PKI-Paths")
111+
cls.auth_config.setConfig('certpath', cls.sslcert)
112+
cls.auth_config.setConfig('keypath', cls.sslkey)
113+
cls.auth_config.setName('test_pki_auth_config')
114+
cls.username = 'Gerardus'
115+
cls.sslrootcert = QSslCertificate.fromPath(cls.sslrootcert_path)
116+
assert cls.sslrootcert is not None
117+
authm.storeCertAuthorities(cls.sslrootcert)
118+
authm.rebuildCaCertsCache()
119+
authm.rebuildTrustedCaCertsCache()
120+
authm.rebuildCertTrustCache()
121+
assert (authm.storeAuthenticationConfig(cls.auth_config)[0])
122+
assert cls.auth_config.isValid()
123+
124+
# Server side
125+
cls.server_cert = os.path.join(cls.certsdata_path, 'localhost_ssl_cert.pem')
126+
cls.server_key = os.path.join(cls.certsdata_path, 'localhost_ssl_key.pem')
127+
cls.server_rootcert = cls.sslrootcert_path
128+
os.chmod(cls.server_cert, stat.S_IRUSR)
129+
os.chmod(cls.server_key, stat.S_IRUSR)
130+
os.chmod(cls.server_rootcert, stat.S_IRUSR)
131+
132+
# Place conf in the data folder
133+
with open(cls.pg_conf, 'w+') as f:
134+
f.write(QGIS_POSTGRES_CONF_TEMPLATE % {
135+
'port': cls.port,
136+
'tempfolder': cls.tempfolder,
137+
'server_cert': cls.server_cert,
138+
'server_key': cls.server_key,
139+
'sslrootcert_path': cls.sslrootcert_path,
140+
})
141+
142+
with open(cls.pg_hba, 'w+') as f:
143+
f.write(QGIS_POSTGRES_HBA_TEMPLATE)
144+
145+
@classmethod
146+
def setUpClass(cls):
147+
"""Run before all tests:
148+
Creates an auth configuration"""
149+
cls.port = QGIS_POSTGRES_SERVER_PORT
150+
cls.dbname = 'test_pki'
151+
cls.tempfolder = QGIS_PG_TEST_PATH
152+
cls.certsdata_path = os.path.join(unitTestDataPath('auth_system'), 'certs_keys')
153+
cls.hostname = 'localhost'
154+
cls.data_path = os.path.join(cls.tempfolder, 'data')
155+
os.mkdir(cls.data_path)
156+
157+
cls.setUpAuth()
158+
subprocess.check_call([os.path.join(QGIS_POSTGRES_EXECUTABLE_PATH, 'initdb'), '-D', cls.data_path])
159+
160+
cls.server = subprocess.Popen([os.path.join(QGIS_POSTGRES_EXECUTABLE_PATH, 'postgres'), '-D',
161+
cls.data_path, '-c',
162+
"config_file=%s" % cls.pg_conf],
163+
env=os.environ,
164+
stdout=subprocess.PIPE,
165+
stderr=subprocess.PIPE)
166+
# Wait max 10 secs for the server to start
167+
end = time.time() + 10
168+
while True:
169+
line = cls.server.stderr.readline()
170+
print(line)
171+
if line.find(b"database system is ready to accept") != -1:
172+
break
173+
if time.time() > end:
174+
raise Exception("Timeout connecting to postgresql")
175+
# Create a DB
176+
subprocess.check_call([os.path.join(QGIS_POSTGRES_EXECUTABLE_PATH, 'createdb'), '-h', 'localhost', '-p', cls.port, 'test_pki'])
177+
# Inject test SQL from test path
178+
test_sql = os.path.join(unitTestDataPath('provider'), 'testdata_pg.sql')
179+
subprocess.check_call([os.path.join(QGIS_POSTGRES_EXECUTABLE_PATH, 'psql'), '-h', 'localhost', '-p', cls.port, '-f', test_sql, cls.dbname])
180+
# Create a role
181+
subprocess.check_call([os.path.join(QGIS_POSTGRES_EXECUTABLE_PATH, 'psql'), '-h', 'localhost', '-p', cls.port, '-c', 'CREATE ROLE "%s" WITH SUPERUSER LOGIN' % cls.username, cls.dbname])
182+
183+
@classmethod
184+
def tearDownClass(cls):
185+
"""Run after all tests"""
186+
cls.server.terminate()
187+
os.kill(cls.server.pid, signal.SIGABRT)
188+
del cls.server
189+
time.sleep(2)
190+
rmtree(QGIS_AUTH_DB_DIR_PATH)
191+
rmtree(cls.tempfolder)
192+
193+
def setUp(self):
194+
"""Run before each test."""
195+
pass
196+
197+
def tearDown(self):
198+
"""Run after each test."""
199+
pass
200+
201+
@classmethod
202+
def _getPostGISLayer(cls, type_name, layer_name=None, authcfg=None):
203+
"""
204+
PG layer factory
205+
"""
206+
if layer_name is None:
207+
layer_name = 'pg_' + type_name
208+
uri = QgsDataSourceURI()
209+
uri.setWkbType(QgsWKBTypes.Point)
210+
uri.setConnection("localhost", cls.port, cls.dbname, "", "", QgsDataSourceURI.SSLrequire, authcfg)
211+
uri.setKeyColumn('pk')
212+
uri.setSrid('EPSG:4326')
213+
uri.setDataSource('qgis_test', 'someData', "geom", "", "pk")
214+
# Note: do not expand here!
215+
layer = QgsVectorLayer(uri.uri(False), layer_name, 'postgres')
216+
return layer
217+
218+
def testValidAuthAccess(self):
219+
"""
220+
Access the protected layer with valid credentials
221+
"""
222+
pg_layer = self._getPostGISLayer('testlayer_èé', authcfg=self.auth_config.id())
223+
self.assertTrue(pg_layer.isValid())
224+
225+
def testInvalidAuthAccess(self):
226+
"""
227+
Access the protected layer with not valid credentials
228+
"""
229+
pg_layer = self._getPostGISLayer('testlayer_èé')
230+
self.assertFalse(pg_layer.isValid())
231+
232+
if __name__ == '__main__':
233+
unittest.main()

‎tests/src/python/test_qgsserver.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def responseComplete(self):
152152

153153
# WMS tests
154154
def wms_request_compare(self, request, extra=None, reference_file=None):
155-
project = self.testdata_path + "test+project.qgs"
155+
project = self.testdata_path + "test_project.qgs"
156156
assert os.path.exists(project), "Project file not found: " + project
157157

158158
query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3&REQUEST=%s' % (urllib.quote(project), request)
@@ -206,7 +206,7 @@ def test_project_wms(self):
206206

207207
def wms_inspire_request_compare(self, request):
208208
"""WMS INSPIRE tests"""
209-
project = self.testdata_path + "test+project_inspire.qgs"
209+
project = self.testdata_path + "test_project_inspire.qgs"
210210
assert os.path.exists(project), "Project file not found: " + project
211211

212212
query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3.0&REQUEST=%s' % (urllib.quote(project), request)
@@ -235,7 +235,7 @@ def test_project_wms_inspire(self):
235235

236236
# WFS tests
237237
def wfs_request_compare(self, request):
238-
project = self.testdata_path + "test+project_wfs.qgs"
238+
project = self.testdata_path + "test_project_wfs.qgs"
239239
assert os.path.exists(project), "Project file not found: " + project
240240

241241
query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % (urllib.quote(project), request)
@@ -269,7 +269,7 @@ def test_project_wfs(self):
269269
self.wfs_request_compare(request)
270270

271271
def wfs_getfeature_compare(self, requestid, request):
272-
project = self.testdata_path + "test+project_wfs.qgs"
272+
project = self.testdata_path + "test_project_wfs.qgs"
273273
assert os.path.exists(project), "Project file not found: " + project
274274

275275
query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % (urllib.quote(project), request)
@@ -316,7 +316,7 @@ def test_getfeature(self):
316316
self.wfs_getfeature_compare(id, req)
317317

318318
def wfs_getfeature_post_compare(self, requestid, request):
319-
project = self.testdata_path + "test+project_wfs.qgs"
319+
project = self.testdata_path + "test_project_wfs.qgs"
320320
assert os.path.exists(project), "Project file not found: " + project
321321

322322
query_string = 'MAP={}'.format(urllib.quote(project))
@@ -361,7 +361,7 @@ def test_getfeature_post(self):
361361
def test_getLegendGraphics(self):
362362
"""Test that does not return an exception but an image"""
363363
parms = {
364-
'MAP': self.testdata_path + "test%2Bproject.qgs",
364+
'MAP': self.testdata_path + "test_project.qgs",
365365
'SERVICE': 'WMS',
366366
'VERSIONE': '1.0.0',
367367
'REQUEST': 'GetLegendGraphic',
@@ -378,7 +378,7 @@ def test_getLegendGraphics(self):
378378
def test_getLegendGraphics_layertitle(self):
379379
"""Test that does not return an exception but an image"""
380380
parms = {
381-
'MAP': self.testdata_path + "test%2Bproject.qgs",
381+
'MAP': self.testdata_path + "test_project.qgs",
382382
'SERVICE': 'WMS',
383383
'VERSION': '1.3.0',
384384
'REQUEST': 'GetLegendGraphic',
@@ -393,7 +393,7 @@ def test_getLegendGraphics_layertitle(self):
393393
self._img_diff_error(r, h, "WMS_GetLegendGraphic_test", 250, QSize(10, 10))
394394

395395
parms = {
396-
'MAP': self.testdata_path + "test%2Bproject.qgs",
396+
'MAP': self.testdata_path + "test_project.qgs",
397397
'SERVICE': 'WMS',
398398
'VERSION': '1.3.0',
399399
'REQUEST': 'GetLegendGraphic',

‎tests/src/python/utilities.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
import glob
1818
import platform
1919
import tempfile
20+
try:
21+
from urllib2 import urlopen, HTTPError, URLError
22+
except ImportError:
23+
from urllib.request import urlopen, HTTPError, URLError
2024

2125
from PyQt4.QtCore import QSize, QDir
2226
from PyQt4.QtGui import QWidget
@@ -748,3 +752,22 @@ def memberIsDocumented(self, member_elem):
748752
if doc is not None and list(doc):
749753
return True
750754
return False
755+
756+
757+
def waitServer(url, timeout=10):
758+
""" Wait for a server to be online and to respond
759+
HTTP errors are ignored
760+
@param timeout: in seconds
761+
@return: True of False
762+
"""
763+
from time import time as now
764+
end = now() + timeout
765+
while True:
766+
try:
767+
urlopen(url, timeout=1)
768+
return True
769+
except (HTTPError, URLError):
770+
return True
771+
except Exception as e:
772+
if now() > end:
773+
return False

‎tests/testdata/qgis_server/getprojectsettings.txt

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ Content-Length: 6268
22
Content-Type: text/xml; charset=utf-8
33

44
<?xml version="1.0" encoding="utf-8"?>
5-
<WMS_Capabilities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.3.0" xmlns="http://www.opengis.net/wms" xsi:schemaLocation="http://www.opengis.net/wms http://schemas.opengis.net/wms/1.3.0/capabilities_1_3_0.xsd http://www.opengis.net/sld http://schemas.opengis.net/sld/1.1.0/sld_capabilities.xsd http://www.qgis.org/wms http:?MAP=/home/ale/dev/QGIS/tests/testdata/qgis_server/test%2Bproject.qgs&amp;SERVICE=WMS&amp;REQUEST=GetSchemaExtension" xmlns:sld="http://www.opengis.net/sld" xmlns:qgs="http://www.qgis.org/wms">
5+
<WMS_Capabilities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.3.0" xmlns="http://www.opengis.net/wms" xsi:schemaLocation="http://www.opengis.net/wms http://schemas.opengis.net/wms/1.3.0/capabilities_1_3_0.xsd http://www.opengis.net/sld http://schemas.opengis.net/sld/1.1.0/sld_capabilities.xsd http://www.qgis.org/wms http:?&amp;SERVICE=WMS&amp;REQUEST=GetSchemaExtension" xmlns:sld="http://www.opengis.net/sld" xmlns:qgs="http://www.qgis.org/wms">
66
<Service>
77
<Name>WMS</Name>
88
<Title>QGIS TestProject</Title>
@@ -30,7 +30,7 @@ Content-Type: text/xml; charset=utf-8
3030
<DCPType>
3131
<HTTP>
3232
<Get>
33-
<OnlineResource xlink:type="simple" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http:?MAP=/home/ale/dev/QGIS/tests/testdata/qgis_server/test%2Bproject.qgs&amp;"/>
33+
<OnlineResource xlink:type="simple" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http:?&amp;"/>
3434
</Get>
3535
</HTTP>
3636
</DCPType>
@@ -45,7 +45,7 @@ Content-Type: text/xml; charset=utf-8
4545
<DCPType>
4646
<HTTP>
4747
<Get>
48-
<OnlineResource xlink:type="simple" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http:?MAP=/home/ale/dev/QGIS/tests/testdata/qgis_server/test%2Bproject.qgs&amp;"/>
48+
<OnlineResource xlink:type="simple" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http:?&amp;"/>
4949
</Get>
5050
</HTTP>
5151
</DCPType>
@@ -59,7 +59,7 @@ Content-Type: text/xml; charset=utf-8
5959
<DCPType>
6060
<HTTP>
6161
<Get>
62-
<OnlineResource xlink:type="simple" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http:?MAP=/home/ale/dev/QGIS/tests/testdata/qgis_server/test%2Bproject.qgs&amp;"/>
62+
<OnlineResource xlink:type="simple" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http:?&amp;"/>
6363
</Get>
6464
</HTTP>
6565
</DCPType>
@@ -70,7 +70,7 @@ Content-Type: text/xml; charset=utf-8
7070
<DCPType>
7171
<HTTP>
7272
<Get>
73-
<OnlineResource xlink:type="simple" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http:?MAP=/home/ale/dev/QGIS/tests/testdata/qgis_server/test%2Bproject.qgs&amp;"/>
73+
<OnlineResource xlink:type="simple" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http:?&amp;"/>
7474
</Get>
7575
</HTTP>
7676
</DCPType>
@@ -80,7 +80,7 @@ Content-Type: text/xml; charset=utf-8
8080
<DCPType>
8181
<HTTP>
8282
<Get>
83-
<OnlineResource xlink:type="simple" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http:?MAP=/home/ale/dev/QGIS/tests/testdata/qgis_server/test%2Bproject.qgs&amp;"/>
83+
<OnlineResource xlink:type="simple" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http:?&amp;"/>
8484
</Get>
8585
</HTTP>
8686
</DCPType>
@@ -90,7 +90,7 @@ Content-Type: text/xml; charset=utf-8
9090
<DCPType>
9191
<HTTP>
9292
<Get>
93-
<OnlineResource xlink:type="simple" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http:?MAP=/home/ale/dev/QGIS/tests/testdata/qgis_server/test%2Bproject.qgs&amp;"/>
93+
<OnlineResource xlink:type="simple" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http:?&amp;"/>
9494
</Get>
9595
</HTTP>
9696
</DCPType>
@@ -102,7 +102,7 @@ Content-Type: text/xml; charset=utf-8
102102
<DCPType>
103103
<HTTP>
104104
<Get>
105-
<OnlineResource xlink:type="simple" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http:?MAP=/home/ale/dev/QGIS/tests/testdata/qgis_server/test%2Bproject.qgs&amp;"/>
105+
<OnlineResource xlink:type="simple" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http:?&amp;"/>
106106
</Get>
107107
</HTTP>
108108
</DCPType>
@@ -112,6 +112,9 @@ Content-Type: text/xml; charset=utf-8
112112
<Format>text/xml</Format>
113113
</Exception>
114114
<sld:UserDefinedSymbolization RemoteWFS="0" RemoteWCS="0" InlineFeature="0" UserStyle="1" SupportSLD="1" UserLayer="0"/>
115+
<WFSLayers>
116+
<WFSLayer name="testlayer èé"/>
117+
</WFSLayers>
115118
<Layer queryable="1">
116119
<Name>QGIS Test Project</Name>
117120
<Title>QGIS Test Project</Title>
@@ -145,7 +148,7 @@ Content-Type: text/xml; charset=utf-8
145148
<Title>default</Title>
146149
<LegendURL>
147150
<Format>image/png</Format>
148-
<OnlineResource xlink:type="simple" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http:?MAP=/home/ale/dev/QGIS/tests/testdata/qgis_server/test%2Bproject.qgs&amp;&amp;SERVICE=WMS&amp;VERSION=1.3.0&amp;REQUEST=GetLegendGraphic&amp;LAYER=testlayer èé&amp;FORMAT=image/png&amp;STYLE=default&amp;SLD_VERSION=1.1.0"/>
151+
<OnlineResource xlink:type="simple" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http:?&amp;&amp;SERVICE=WMS&amp;VERSION=1.3.0&amp;REQUEST=GetLegendGraphic&amp;LAYER=testlayer èé&amp;FORMAT=image/png&amp;STYLE=default&amp;SLD_VERSION=1.1.0"/>
149152
</LegendURL>
150153
</Style>
151154
<TreeName>testlayer èé</TreeName>

‎tests/testdata/qgis_server/test+project.qgs renamed to ‎tests/testdata/qgis_server/test_project.qgs

Lines changed: 145 additions & 94 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.