def do_GET(self): parsed_path = urlparse.urlparse(self.path) s = QgsServer() headers, body = s.handleRequest(parsed_path.query) self.send_response(200) for k, v in [h.split(':') for h in headers.split('\n') if h]: self.send_header(k, v) self.end_headers() self.wfile.write(body) return
def __init__(self) -> None: self.cachedir = request.config.rootdir.join('__cadastre___') self.datapath = request.config.rootdir.join('data') os.environ['QGIS_CADASTRE_CACHE_DIR'] = self.cachedir.strpath self.server = QgsServer() # Load plugins load_plugins(self.server.serverInterface())
def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer()
def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' d = unitTestDataPath('qgis_server_accesscontrol') + '/' self.projectPath = os.path.join(d, "project.qgs") # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer()
class _Client: def __init__(self) -> None: self.cachedir = request.config.rootdir.join('__cadastre___') self.datapath = request.config.rootdir.join('data') os.environ['QGIS_CADASTRE_CACHE_DIR'] = self.cachedir.strpath self.server = QgsServer() # Load plugins load_plugins(self.server.serverInterface()) def getplugin(self, name) -> Any: """ retourne l'instance du plugin """ return server_plugins.get(name) def getprojectpath(self, name: str) -> str: return self.datapath.join(name) def get(self, query: str, project: str=None) -> _Response: """ Return server response from query """ request = QgsBufferServerRequest(query, QgsServerRequest.GetMethod, {}, None) response = QgsBufferServerResponse() if project is not None and not os.path.isabs(project): projectpath = self.datapath.join(project) qgsproject = QgsProject() if not qgsproject.read(projectpath.strpath): raise ValueError("Error reading project '%s':" % projectpath.strpath) else: qgsproject = None self.server.handleRequest(request, response, project=qgsproject) return _Response(response)
def test_configpath(self): """ Test plugin can read confif path """ try: from qgis.server import QgsServerFilter from qgis.core import QgsProject except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return d = unitTestDataPath('qgis_server_accesscontrol') + '/' self.projectPath = os.path.join(d, "project.qgs") self.server = QgsServer() # global to be modified inside plugin filters globals()['configFilePath2'] = None class Filter0(QgsServerFilter): """Body setter, clear body, keep headers""" def requestReady(self): global configFilePath2 configFilePath2 = self.serverInterface().configFilePath() serverIface = self.server.serverInterface() serverIface.registerFilter(Filter0(serverIface), 100) # Test using MAP self._execute_request('?service=simple&MAP=%s' % self.projectPath) # Check config file path self.assertEqual(configFilePath2, self.projectPath) # Reset result globals()['configFilePath2'] = None # Test with prqject as argument project = QgsProject() project.read(self.projectPath) self._execute_request_project('?service=simple', project=project) # Check config file path self.assertEqual(configFilePath2, project.fileName())
def setUp(self): """Create the server instance""" self.fontFamily = QgsFontUtils.standardTestFontFamily() QgsFontUtils.loadStandardTestFonts(['All']) self.testdata_path = unitTestDataPath('qgis_server') + '/' d = unitTestDataPath('qgis_server_accesscontrol') + '/' self.projectPath = os.path.join(d, "project.qgs") self.projectAnnotationPath = os.path.join(d, "project_with_annotations.qgs") self.projectStatePath = os.path.join(d, "project_state.qgs") self.projectUseLayerIdsPath = os.path.join(d, "project_use_layerids.qgs") # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer()
class TestModules(unittest.TestCase): """ """ @classmethod def setUpClass(cls): cls.app = QgsApplication([], False) @classmethod def tearDownClass(cls): cls.app.exitQgis() def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' d = unitTestDataPath('qgis_server_accesscontrol') + '/' self.projectPath = os.path.join(d, "project.qgs") # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def test_modules(self): """ Tests that modules are loaded """ # Check that our 'SampleService is registered iface = self.server.serverInterface() service = iface.serviceRegistry().getService('SampleService') self.assertIsNotNone(service)
class TestQgsServer(unittest.TestCase): def assertXMLEqual(self, response, expected, msg=''): """Compare XML line by line and sorted attributes""" response_lines = response.splitlines() expected_lines = expected.splitlines() line_no = 1 for expected_line in expected_lines: expected_line = expected_line.strip() response_line = response_lines[line_no - 1].strip() # Compare tag try: self.assertEqual(re.findall(b'<([^>\s]+)[ >]', expected_line)[0], re.findall(b'<([^>\s]+)[ >]', response_line)[0], msg=msg + "\nTag mismatch on line %s: %s != %s" % (line_no, expected_line, response_line)) except IndexError: self.assertEqual(expected_line, response_line, msg=msg + "\nTag line mismatch %s: %s != %s" % (line_no, expected_line, response_line)) #print("---->%s\t%s == %s" % (line_no, expected_line, response_line)) # Compare attributes if re.match(RE_ATTRIBUTES, expected_line): # has attrs expected_attrs = sorted(re.findall(RE_ATTRIBUTES, expected_line)) response_attrs = sorted(re.findall(RE_ATTRIBUTES, response_line)) self.assertEqual(expected_attrs, response_attrs, msg=msg + "\nXML attributes differ at line {0}: {1} != {2}".format(line_no, expected_attrs, response_attrs)) line_no += 1 @classmethod def setUpClass(cls): cls.app = QgsApplication([], False) @classmethod def tearDownClass(cls): cls.app.exitQgis() def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' d = unitTestDataPath('qgis_server_accesscontrol') + '/' self.projectPath = os.path.join(d, "project.qgs") # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def strip_version_xmlns(self, text): """Order of attributes is random, strip version and xmlns""" return text.replace(b'version="1.3.0"', b'').replace(b'xmlns="http://www.opengis.net/ogc"', b'') def assert_headers(self, header, body): stream = StringIO() header_string = header.decode('utf-8') stream.write(header_string) headers = email.message_from_string(header_string) if 'content-length' in headers: content_length = int(headers['content-length']) body_length = len(body) self.assertEqual(content_length, body_length, msg="Header reported content-length: %d Actual body length was: %d" % (content_length, body_length)) def test_destructor_segfaults(self): """Segfault on destructor?""" server = QgsServer() del server def test_multiple_servers(self): """Segfaults?""" for i in range(10): locals()["s%s" % i] = QgsServer() locals()["s%s" % i].handleRequest("") def test_api(self): """Using an empty query string (returns an XML exception) we are going to test if headers and body are returned correctly""" # Test as a whole header, body = [_v for _v in self.server.handleRequest("")] response = self.strip_version_xmlns(header + body) expected = self.strip_version_xmlns(b'Content-Length: 54\nContent-Type: text/xml; charset=utf-8\n\n<ServerException>Project file error</ServerException>\n') self.assertEqual(response, expected) expected = b'Content-Length: 54\nContent-Type: text/xml; charset=utf-8\n\n' self.assertEqual(header, expected) # Test response when project is specified but without service project = self.testdata_path + "test_project_wfs.qgs" qs = 'MAP=%s' % (urllib.parse.quote(project)) header, body = [_v for _v in self.server.handleRequest(qs)] response = self.strip_version_xmlns(header + body) expected = self.strip_version_xmlns(b'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n') self.assertEqual(response, expected) expected = b'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n' self.assertEqual(header, expected) # Test body expected = self.strip_version_xmlns(b'<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n') self.assertEqual(self.strip_version_xmlns(body), expected) def test_pluginfilters(self): """Test python plugins filters""" try: from qgis.server import QgsServerFilter except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return class SimpleHelloFilter(QgsServerFilter): def requestReady(self): QgsMessageLog.logMessage("SimpleHelloFilter.requestReady") def sendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.sendResponse") def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() QgsMessageLog.logMessage("SimpleHelloFilter.responseComplete") if params.get('SERVICE', '').upper() == 'SIMPLE': request.clear() request.setHeader('Content-type', 'text/plain') request.appendBody('Hello from SimpleServer!'.encode('utf-8')) serverIface = self.server.serverInterface() filter = SimpleHelloFilter(serverIface) serverIface.registerFilter(filter, 100) # Get registered filters self.assertEqual(filter, serverIface.filters()[100][0]) # Register some more filters class Filter1(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter1!'.encode('utf-8')) class Filter2(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter2!'.encode('utf-8')) filter1 = Filter1(serverIface) filter2 = Filter2(serverIface) serverIface.registerFilter(filter1, 101) serverIface.registerFilter(filter2, 200) serverIface.registerFilter(filter2, 100) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [_v for _v in self.server.handleRequest('service=simple')] response = header + body expected = b'Content-Length: 62\nContent-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Test that the bindings for complex type QgsServerFiltersMap are working filters = {100: [filter, filter2], 101: [filter1], 200: [filter2]} serverIface.setFilters(filters) self.assertTrue(filter in serverIface.filters()[100]) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [_v for _v in self.server.handleRequest('service=simple')] response = header + body expected = b'Content-Length: 62\nContent-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # WMS tests def wms_request_compare(self, request, extra=None, reference_file=None): project = self.testdata_path + "test_project.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3&REQUEST=%s' % (urllib.parse.quote(project), request) if extra is not None: query_string += extra header, body = self.server.handleRequest(query_string) response = header + body reference_path = self.testdata_path + (request.lower() if not reference_file else reference_file) + '.txt' f = open(reference_path, 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(reference_path, 'wb+') f.write(response) f.close() f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() #""" response = re.sub(RE_STRIP_UNCHECKABLE, b'*****', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'*****', expected) self.assertXMLEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected.decode('utf-8'), response.decode('utf-8'))) def test_project_wms(self): """Test some WMS request""" for request in ('GetCapabilities', 'GetProjectSettings'): self.wms_request_compare(request) # Test getfeatureinfo response self.wms_request_compare('GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + 'info_format=text%2Fhtml&transparent=true&' + 'width=600&height=400&srs=EPSG%3A3857&bbox=913190.6389747962%2C' + '5606005.488876367%2C913235.426296057%2C5606035.347090538&' + 'query_layers=testlayer%20%C3%A8%C3%A9&X=190&Y=320', 'wms_getfeatureinfo-text-html') # Test getfeatureinfo default info_format self.wms_request_compare('GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + 'transparent=true&' + 'width=600&height=400&srs=EPSG%3A3857&bbox=913190.6389747962%2C' + '5606005.488876367%2C913235.426296057%2C5606035.347090538&' + 'query_layers=testlayer%20%C3%A8%C3%A9&X=190&Y=320', 'wms_getfeatureinfo-text-plain') # Regression for #8656 # Mind the gap! (the space in the FILTER expression) self.wms_request_compare('GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&' + 'INFO_FORMAT=text%2Fxml&' + 'width=600&height=400&srs=EPSG%3A3857&' + 'query_layers=testlayer%20%C3%A8%C3%A9&' + 'FEATURE_COUNT=10&FILTER=testlayer%20%C3%A8%C3%A9' + urllib.parse.quote(':"NAME" = \'two\''), 'wms_getfeatureinfo_filter') def wms_inspire_request_compare(self, request): """WMS INSPIRE tests""" project = self.testdata_path + "test_project_inspire.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3.0&REQUEST=%s' % (urllib.parse.quote(project), request) header, body = self.server.handleRequest(query_string) response = header + body f = open(self.testdata_path + request.lower() + '_inspire.txt', 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_UNCHECKABLE, b'', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'', expected) self.assertXMLEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected.decode('utf-8'), response.decode('utf-8'))) def test_project_wms_inspire(self): """Test some WMS request""" for request in ('GetCapabilities',): self.wms_inspire_request_compare(request) # WFS tests def wfs_request_compare(self, request): project = self.testdata_path + "test_project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % (urllib.parse.quote(project), request) header, body = self.server.handleRequest(query_string) self.assert_headers(header, body) response = header + body f = open(self.testdata_path + 'wfs_' + request.lower() + '.txt', 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_UNCHECKABLE, b'', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'', expected) self.assertXMLEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected.decode('utf-8'), response.decode('utf-8'))) def test_project_wfs(self): """Test some WFS request""" for request in ('GetCapabilities', 'DescribeFeatureType'): self.wfs_request_compare(request) def wfs_getfeature_compare(self, requestid, request): project = self.testdata_path + "test_project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % (urllib.parse.quote(project), request) header, body = self.server.handleRequest(query_string) self.result_compare( 'wfs_getfeature_' + requestid + '.txt', "request %s failed.\n Query: %s" % ( query_string, request, ), header, body ) def test_wfs_getcapabilities_url(self): # empty url in project project = os.path.join(self.testdata_path, "test_project_without_urls.qgs") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(project), "SERVICE": "WFS", "VERSION": "1.3.0", "REQUEST": "GetCapabilities", "STYLES": "" }.items())]) r, h = self._result(self.server.handleRequest(qs)) for item in str(r).split("\\n"): if "onlineResource" in item: self.assertEqual("onlineResource=\"?" in item, True) # url well defined in project project = os.path.join(self.testdata_path, "test_project_with_urls.qgs") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(project), "SERVICE": "WFS", "VERSION": "1.3.0", "REQUEST": "GetCapabilities", "STYLES": "" }.items())]) r, h = self._result(self.server.handleRequest(qs)) for item in str(r).split("\\n"): if "onlineResource" in item: print("onlineResource: ", item) self.assertEqual("onlineResource=\"my_wfs_advertised_url\"" in item, True) def result_compare(self, file_name, error_msg_header, header, body): self.assert_headers(header, body) response = header + body f = open(self.testdata_path + file_name, 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_UNCHECKABLE, b'', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'', expected) self.assertXMLEqual(response, expected, msg="%s\n Expected:\n%s\n\n Response:\n%s" % (error_msg_header, str(expected, errors='replace'), str(response, errors='replace'))) def test_getfeature(self): tests = [] tests.append(('nobbox', 'GetFeature&TYPENAME=testlayer')) tests.append(('startindex2', 'GetFeature&TYPENAME=testlayer&STARTINDEX=2')) tests.append(('limit2', 'GetFeature&TYPENAME=testlayer&MAXFEATURES=2')) tests.append(('start1_limit1', 'GetFeature&TYPENAME=testlayer&MAXFEATURES=1&STARTINDEX=1')) for id, req in tests: self.wfs_getfeature_compare(id, req) def wfs_getfeature_post_compare(self, requestid, request): project = self.testdata_path + "test_project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP={}'.format(urllib.parse.quote(project)) self.server.putenv("REQUEST_METHOD", "POST") self.server.putenv("REQUEST_BODY", request) header, body = self.server.handleRequest(query_string) self.server.putenv("REQUEST_METHOD", '') self.server.putenv("REQUEST_BODY", '') self.result_compare( 'wfs_getfeature_{}.txt'.format(requestid), "GetFeature in POST for '{}' failed.".format(requestid), header, body, ) def test_getfeature_post(self): template = """<?xml version="1.0" encoding="UTF-8"?> <wfs:GetFeature service="WFS" version="1.0.0" {} xmlns:wfs="http://www.opengis.net/wfs" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs http://schemas.opengis.net/wfs/1.1.0/wfs.xsd"> <wfs:Query typeName="testlayer" xmlns:feature="http://www.qgis.org/gml"> <ogc:Filter xmlns:ogc="http://www.opengis.net/ogc"> <ogc:BBOX> <ogc:PropertyName>geometry</ogc:PropertyName> <gml:Envelope xmlns:gml="http://www.opengis.net/gml"> <gml:lowerCorner>8 44</gml:lowerCorner> <gml:upperCorner>9 45</gml:upperCorner> </gml:Envelope> </ogc:BBOX> </ogc:Filter> </wfs:Query> </wfs:GetFeature> """ tests = [] tests.append(('nobbox', template.format(""))) tests.append(('startindex2', template.format('startIndex="2"'))) tests.append(('limit2', template.format('maxFeatures="2"'))) tests.append(('start1_limit1', template.format('startIndex="1" maxFeatures="1"'))) for id, req in tests: self.wfs_getfeature_post_compare(id, req) def test_wms_getmap_basic(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_Basic") def test_wms_getmap_transparent(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "TRANSPARENT": "TRUE" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_Transparent") def test_wms_getmap_background(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "BGCOLOR": "green" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_Background") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "BGCOLOR": "0x008000" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_Background_Hex") def test_wms_getcapabilities_url(self): # empty url in project project = os.path.join(self.testdata_path, "test_project_without_urls.qgs") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(project), "SERVICE": "WMS", "VERSION": "1.3.0", "REQUEST": "GetCapabilities", "STYLES": "" }.items())]) r, h = self._result(self.server.handleRequest(qs)) item_found = False for item in str(r).split("\\n"): if "OnlineResource" in item: self.assertEqual("xlink:href=\"?" in item, True) item_found = True self.assertTrue(item_found) # url well defined in project project = os.path.join(self.testdata_path, "test_project_with_urls.qgs") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(project), "SERVICE": "WMS", "VERSION": "1.3.0", "REQUEST": "GetCapabilities", "STYLES": "" }.items())]) r, h = self._result(self.server.handleRequest(qs)) item_found = False for item in str(r).split("\\n"): if "OnlineResource" in item: self.assertEqual("xlink:href=\"my_wms_advertised_url?" in item, True) item_found = True self.assertTrue(item_found) def test_wms_getmap_invalid_size(self): project = os.path.join(self.testdata_path, "test_project_with_size.qgs") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(project), "SERVICE": "WMS", "VERSION": "1.3.0", "REQUEST": "GetMap", "LAYERS": "Hello", "STYLES": "", "FORMAT": "image/png", "HEIGHT": "5001", "WIDTH": "5000" }.items())]) expected = self.strip_version_xmlns(b'<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Size error">The requested map size is too large</ServiceException>\n</ServiceExceptionReport>\n') r, h = self._result(self.server.handleRequest(qs)) self.assertEqual(self.strip_version_xmlns(r), expected) def test_wms_getmap_order(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Hello,Country", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_LayerOrder") def test_wms_getmap_srs(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country,Hello", "STYLES": "", "FORMAT": "image/png", "BBOX": "-151.7,-38.9,51.0,78.0", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:4326" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_SRS") def test_wms_getmap_style(self): # default style qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country_Labels", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_StyleDefault") # custom style qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country_Labels", "STYLES": "custom", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_StyleCustom") def test_wms_getmap_filter(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country,Hello", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "FILTER": "Country:\"name\" = 'eurasia'" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_Filter") def test_wms_getmap_selection(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country,Hello", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "SRS": "EPSG:3857", "SELECTION": "Country: 4" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_Selection") def test_wms_getmap_opacities(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country,Hello", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "OPACITIES": "125, 50" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_Opacities") def test_wms_getprint_basic(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetPrint", "TEMPLATE": "layoutA4", "FORMAT": "png", "map0:EXTENT": "-33626185.498,-13032965.185,33978427.737,16020257.031", "map0:LAYERS": "Country,Hello", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetPrint_Basic") def test_wms_getprint_srs(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetPrint", "TEMPLATE": "layoutA4", "FORMAT": "png", "map0:EXTENT": "-309.015,-133.011,312.179,133.949", "map0:LAYERS": "Country,Hello", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:4326" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetPrint_SRS") def test_wms_getprint_scale(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetPrint", "TEMPLATE": "layoutA4", "FORMAT": "png", "map0:EXTENT": "-33626185.498,-13032965.185,33978427.737,16020257.031", "map0:LAYERS": "Country,Hello", "map0:SCALE": "36293562", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetPrint_Scale") def test_wms_getprint_grid(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetPrint", "TEMPLATE": "layoutA4", "FORMAT": "png", "map0:EXTENT": "-33626185.498,-13032965.185,33978427.737,16020257.031", "map0:LAYERS": "Country,Hello", "map0:GRID_INTERVAL_X": "1000000", "map0:GRID_INTERVAL_Y": "2000000", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetPrint_Grid") def test_wms_getprint_rotation(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetPrint", "TEMPLATE": "layoutA4", "FORMAT": "png", "map0:EXTENT": "-33626185.498,-13032965.185,33978427.737,16020257.031", "map0:LAYERS": "Country,Hello", "map0:ROTATION": "45", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetPrint_Rotation") def test_wms_getprint_selection(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetPrint", "TEMPLATE": "layoutA4", "FORMAT": "png", "map0:EXTENT": "-33626185.498,-13032965.185,33978427.737,16020257.031", "map0:LAYERS": "Country,Hello", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "SELECTION": "Country: 4" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetPrint_Selection") def test_getLegendGraphics(self): """Test that does not return an exception but an image""" parms = { 'MAP': self.testdata_path + "test_project.qgs", 'SERVICE': 'WMS', 'VERSION': '1.3.0', 'REQUEST': 'GetLegendGraphic', 'FORMAT': 'image/png', #'WIDTH': '20', # optional #'HEIGHT': '20', # optional 'LAYER': 'testlayer%20èé', } qs = '&'.join(["%s=%s" % (k, v) for k, v in parms.items()]) h, r = self.server.handleRequest(qs) self.assertEqual(-1, h.find(b'Content-Type: text/xml; charset=utf-8'), "Header: %s\nResponse:\n%s" % (h, r)) self.assertNotEqual(-1, h.find(b'Content-Type: image/png'), "Header: %s\nResponse:\n%s" % (h, r)) def test_getLegendGraphics_layertitle(self): """Test that does not return an exception but an image""" parms = { 'MAP': self.testdata_path + "test_project.qgs", 'SERVICE': 'WMS', 'VERSION': '1.3.0', 'REQUEST': 'GetLegendGraphic', 'FORMAT': 'image/png', #'WIDTH': '20', # optional #'HEIGHT': '20', # optional 'LAYER': u'testlayer%20èé', 'LAYERTITLE': 'TRUE', } qs = '&'.join([u"%s=%s" % (k, v) for k, v in parms.items()]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_test", 250, QSize(10, 10)) parms = { 'MAP': self.testdata_path + "test_project.qgs", 'SERVICE': 'WMS', 'VERSION': '1.3.0', 'REQUEST': 'GetLegendGraphic', 'FORMAT': 'image/png', #'WIDTH': '20', # optional #'HEIGHT': '20', # optional 'LAYER': u'testlayer%20èé', 'LAYERTITLE': 'FALSE', } qs = '&'.join([u"%s=%s" % (k, v) for k, v in parms.items()]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_test_layertitle_false", 250, QSize(10, 10)) def test_wms_GetLegendGraphic_Basic(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_Basic") def test_wms_GetLegendGraphic_Transparent(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "TRANSPARENT": "TRUE" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_Transparent") def test_wms_GetLegendGraphic_Background(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "BGCOLOR": "green" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_Background") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "BGCOLOR": "0x008000" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_Background_Hex") def test_wms_GetLegendGraphic_BoxSpace(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "BOXSPACE": "100", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_BoxSpace") def test_wms_GetLegendGraphic_SymbolSpace(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "SYMBOLSPACE": "100", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_SymbolSpace") def test_wms_GetLegendGraphic_IconLabelSpace(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "ICONLABELSPACE": "100", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_IconLabelSpace") def test_wms_GetLegendGraphic_SymbolSize(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "SYMBOLWIDTH": "50", "SYMBOLHEIGHT": "30", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_SymbolSize") def test_wms_GetLegendGraphic_BBox(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello,db_point", "LAYERTITLE": "FALSE", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "BBOX": "-151.7,-38.9,51.0,78.0", "CRS": "EPSG:4326" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_BBox") def test_wms_GetLegendGraphic_BBox2(self): qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello,db_point", "LAYERTITLE": "FALSE", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "BBOX": "-76.08,-6.4,-19.38,38.04", "SRS": "EPSG:4326" }.items())]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_BBox2") def test_wcs_getcapabilities_url(self): # empty url in project project = os.path.join(self.testdata_path, "test_project_without_urls.qgs") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(project), "SERVICE": "WCS", "VERSION": "1.3.0", "REQUEST": "GetCapabilities", "STYLES": "" }.items())]) r, h = self._result(self.server.handleRequest(qs)) item_found = False for item in str(r).split("\\n"): if "OnlineResource" in item: self.assertEqual("=\"?" in item, True) item_found = True self.assertTrue(item_found) # url well defined in project project = os.path.join(self.testdata_path, "test_project_with_urls.qgs") qs = "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(project), "SERVICE": "WCS", "VERSION": "1.3.0", "REQUEST": "GetCapabilities", "STYLES": "" }.items())]) r, h = self._result(self.server.handleRequest(qs)) item_found = False for item in str(r).split("\\n"): if "OnlineResource" in item: print("OnlineResource: ", item) self.assertEqual("\"my_wcs_advertised_url" in item, True) item_found = True self.assertTrue(item_found) def _result(self, data): headers = {} for line in data[0].decode('UTF-8').split("\n"): if line != "": header = line.split(":") self.assertEqual(len(header), 2, line) headers[str(header[0])] = str(header[1]).strip() return data[1], headers def _img_diff(self, image, control_image, max_diff, max_size_diff=QSize()): temp_image = os.path.join(tempfile.gettempdir(), "%s_result.png" % control_image) with open(temp_image, "wb") as f: f.write(image) control = QgsRenderChecker() control.setControlPathPrefix("qgis_server") control.setControlName(control_image) control.setRenderedImage(temp_image) if max_size_diff.isValid(): control.setSizeTolerance(max_size_diff.width(), max_size_diff.height()) return control.compareImages(control_image), control.report() def _img_diff_error(self, response, headers, image, max_diff=10, max_size_diff=QSize()): self.assertEqual( headers.get("Content-Type"), "image/png", "Content type is wrong: %s" % headers.get("Content-Type")) test, report = self._img_diff(response, image, max_diff, max_size_diff) with open(os.path.join(tempfile.gettempdir(), image + "_result.png"), "rb") as rendered_file: encoded_rendered_file = base64.b64encode(rendered_file.read()) message = "Image is wrong\n%s\nImage:\necho '%s' | base64 -d >%s/%s_result.png" % ( report, encoded_rendered_file.strip(), tempfile.gettempdir(), image ) # If the failure is in image sizes the diff file will not exists. if os.path.exists(os.path.join(tempfile.gettempdir(), image + "_result_diff.png")): with open(os.path.join(tempfile.gettempdir(), image + "_result_diff.png"), "rb") as diff_file: encoded_diff_file = base64.b64encode(diff_file.read()) message += "\nDiff:\necho '%s' | base64 -d > %s/%s_result_diff.png" % ( encoded_diff_file.strip(), tempfile.gettempdir(), image ) self.assertTrue(test, message)
class TestQgsServerPlugins(unittest.TestCase): def assertXMLEqual(self, response, expected, msg=''): """Compare XML line by line and sorted attributes""" response_lines = response.splitlines() expected_lines = expected.splitlines() line_no = 1 for expected_line in expected_lines: expected_line = expected_line.strip() response_line = response_lines[line_no - 1].strip() # Compare tag try: self.assertEqual( re.findall(b'<([^>\s]+)[ >]', expected_line)[0], re.findall(b'<([^>\s]+)[ >]', response_line)[0], msg=msg + "\nTag mismatch on line %s: %s != %s" % (line_no, expected_line, response_line)) except IndexError: self.assertEqual(expected_line, response_line, msg=msg + "\nTag line mismatch %s: %s != %s" % (line_no, expected_line, response_line)) # print("---->%s\t%s == %s" % (line_no, expected_line, response_line)) # Compare attributes if re.match(RE_ATTRIBUTES, expected_line): # has attrs expected_attrs = sorted( re.findall(RE_ATTRIBUTES, expected_line)) response_attrs = sorted( re.findall(RE_ATTRIBUTES, response_line)) self.assertEqual( expected_attrs, response_attrs, msg=msg + "\nXML attributes differ at line {0}: {1} != {2}".format( line_no, expected_attrs, response_attrs)) line_no += 1 @classmethod def setUpClass(cls): cls.app = QgsApplication([], False) @classmethod def tearDownClass(cls): cls.app.exitQgis() def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' d = unitTestDataPath('qgis_server_accesscontrol') + '/' self.projectPath = os.path.join(d, "project.qgs") # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def strip_version_xmlns(self, text): """Order of attributes is random, strip version and xmlns""" return text.replace(b'version="1.3.0"', b'').replace(b'xmlns="http://www.opengis.net/ogc"', b'') def assert_headers(self, header, body): stream = StringIO() header_string = header.decode('utf-8') stream.write(header_string) headers = email.message_from_string(header_string) if 'content-length' in headers: content_length = int(headers['content-length']) body_length = len(body) self.assertEqual( content_length, body_length, msg= "Header reported content-length: %d Actual body length was: %d" % (content_length, body_length)) def test_pluginfilters(self): """Test python plugins filters""" try: from qgis.server import QgsServerFilter except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return class SimpleHelloFilter(QgsServerFilter): def requestReady(self): QgsMessageLog.logMessage("SimpleHelloFilter.requestReady") def sendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.sendResponse") def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() QgsMessageLog.logMessage("SimpleHelloFilter.responseComplete") if params.get('SERVICE', '').upper() == 'SIMPLE': request.clear() request.setHeader('Content-type', 'text/plain') request.appendBody( 'Hello from SimpleServer!'.encode('utf-8')) serverIface = self.server.serverInterface() filter = SimpleHelloFilter(serverIface) serverIface.registerFilter(filter, 100) # Get registered filters self.assertEqual(filter, serverIface.filters()[100][0]) # global to be modified inside plugin filters globals()['status_code'] = 0 # body to be checked inside plugin filters globals()['body2'] = None # headers to be checked inside plugin filters globals()['headers2'] = None # Register some more filters class Filter1(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter1!'.encode('utf-8')) class Filter2(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter2!'.encode('utf-8')) class Filter3(QgsServerFilter): """Test get and set status code""" def responseComplete(self): global status_code request = self.serverInterface().requestHandler() request.setStatusCode(999) status_code = request.statusCode() class Filter4(QgsServerFilter): """Body getter""" def responseComplete(self): global body2 request = self.serverInterface().requestHandler() body2 = request.body() class Filter5(QgsServerFilter): """Body setter, clear body, keep headers""" def responseComplete(self): global headers2 request = self.serverInterface().requestHandler() request.clearBody() headers2 = {k: request.header(k) for k in request.headerKeys()} request.appendBody('new body, new life!'.encode('utf-8')) filter1 = Filter1(serverIface) filter2 = Filter2(serverIface) filter3 = Filter3(serverIface) filter4 = Filter4(serverIface) serverIface.registerFilter(filter1, 101) serverIface.registerFilter(filter2, 200) serverIface.registerFilter(filter2, 100) serverIface.registerFilter(filter3, 300) serverIface.registerFilter(filter4, 400) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [ _v for _v in self.server.handleRequest('?service=simple') ] response = header + body expected = b'Content-Length: 62\nContent-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Check status code self.assertEqual(status_code, 999) # Check body getter from filter self.assertEqual( body2, b'Hello from SimpleServer!Hello from Filter1!Hello from Filter2!') # Check that the bindings for complex type QgsServerFiltersMap are working filters = {100: [filter, filter2], 101: [filter1], 200: [filter2]} serverIface.setFilters(filters) self.assertTrue(filter in serverIface.filters()[100]) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [ _v for _v in self.server.handleRequest('?service=simple') ] response = header + body expected = b'Content-Length: 62\nContent-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Now, re-run with body setter filter5 = Filter5(serverIface) serverIface.registerFilter(filter5, 500) header, body = [ _v for _v in self.server.handleRequest('?service=simple') ] response = header + body expected = b'Content-Length: 19\nContent-type: text/plain\n\nnew body, new life!' self.assertEqual(response, expected) self.assertEqual(headers2, {'Content-type': 'text/plain'})
and QGIS_SERVER_PKI_USERNAME) # Check if OAuth2 is enabled QGIS_SERVER_OAUTH2_AUTH = (QGIS_SERVER_OAUTH2_CERTIFICATE is not None and os.path.isfile(QGIS_SERVER_OAUTH2_CERTIFICATE) and QGIS_SERVER_OAUTH2_KEY is not None and os.path.isfile(QGIS_SERVER_OAUTH2_KEY) and QGIS_SERVER_OAUTH2_AUTHORITY is not None and os.path.isfile(QGIS_SERVER_OAUTH2_AUTHORITY) and QGIS_SERVER_OAUTH2_USERNAME and QGIS_SERVER_OAUTH2_PASSWORD) HTTPS_ENABLED = QGIS_SERVER_PKI_AUTH or QGIS_SERVER_OAUTH2_AUTH qgs_app = QgsApplication([], False) qgs_server = QgsServer() if QGIS_SERVER_HTTP_BASIC_AUTH: from qgis.server import QgsServerFilter import base64 class HTTPBasicFilter(QgsServerFilter): def requestReady(self): handler = self.serverInterface().requestHandler() auth = self.serverInterface().requestHandler().requestHeader( 'HTTP_AUTHORIZATION') if auth: username, password = base64.b64decode(auth[6:]).split(b':') if (username.decode('utf-8') == os.environ.get( 'QGIS_SERVER_USERNAME', 'username') and password.decode('utf-8') == os.environ.get(
class TestQgsServer(unittest.TestCase): def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def test_destructor_segfaults(self): """Segfault on destructor?""" server = QgsServer() del server def test_multiple_servers(self): """Segfaults?""" for i in range(10): locals()["s%s" % i] = QgsServer() locals()["s%s" % i].handleRequest() def test_api(self): """Using an empty query string (returns an XML exception) we are going to test if headers and body are returned correctly""" # Test as a whole header, body = [str(_v) for _v in self.server.handleRequest()] response = header + body expected = 'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n' self.assertEqual(response, expected) expected = 'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n' self.assertEqual(header, expected) # Test body expected = '<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n' self.assertEqual(body, expected) def test_pluginfilters(self): """Test python plugins filters""" try: from qgis.server import QgsServerFilter except ImportError: print "QGIS Server plugins are not compiled. Skipping test" return class SimpleHelloFilter(QgsServerFilter): def requestReady(self): QgsMessageLog.logMessage("SimpleHelloFilter.requestReady") def sendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.sendResponse") def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() QgsMessageLog.logMessage("SimpleHelloFilter.responseComplete") if params.get('SERVICE', '').upper() == 'SIMPLE': request.clearHeaders() request.setHeader('Content-type', 'text/plain') request.clearBody() request.appendBody('Hello from SimpleServer!') serverIface = self.server.serverInterface() filter = SimpleHelloFilter(serverIface) serverIface.registerFilter(filter, 100) # Get registered filters self.assertEqual(filter, serverIface.filters()[100][0]) # Register some more filters class Filter1(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter1!') class Filter2(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter2!') filter1 = Filter1(serverIface) filter2 = Filter2(serverIface) serverIface.registerFilter(filter1, 101) serverIface.registerFilter(filter2, 200) serverIface.registerFilter(filter2, 100) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [str(_v) for _v in self.server.handleRequest('service=simple')] response = header + body expected = 'Content-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Test that the bindings for complex type QgsServerFiltersMap are working filters = {100: [filter, filter2], 101: [filter1], 200: [filter2]} serverIface.setFilters(filters) self.assertTrue(filter in serverIface.filters()[100]) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [str(_v) for _v in self.server.handleRequest('service=simple')] response = header + body expected = 'Content-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # WMS tests def wms_request_compare(self, request): project = self.testdata_path + "test+project.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3&REQUEST=%s' % (urllib.quote(project), request) header, body = [str(_v) for _v in self.server.handleRequest(query_string)] response = header + body f = open(self.testdata_path + request.lower() + '.txt') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) self.assertEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected, response)) def test_project_wms(self): """Test some WMS request""" for request in ('GetCapabilities', 'GetProjectSettings'): self.wms_request_compare(request) # WMS INSPIRE tests def wms_inspire_request_compare(self, request): project = self.testdata_path + "test+project_inspire.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3.0&REQUEST=%s' % (urllib.quote(project), request) header, body = [str(_v) for _v in self.server.handleRequest(query_string)] response = header + body f = open(self.testdata_path + request.lower() + '_inspire.txt') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) self.assertEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected, response)) def test_project_wms_inspire(self): """Test some WMS request""" for request in ('GetCapabilities',): self.wms_inspire_request_compare(request) # WFS tests def wfs_request_compare(self, request): project = self.testdata_path + "test+project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % (urllib.quote(project), request) header, body = [str(_v) for _v in self.server.handleRequest(query_string)] self.assert_headers(header, body) response = header + body f = open(self.testdata_path + 'wfs_' + request.lower() + '.txt') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) self.assertEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected, response)) def test_project_wfs(self): """Test some WMS request""" for request in ('GetCapabilities', 'DescribeFeatureType'): self.wfs_request_compare(request) def wfs_getfeature_compare(self, requestid, request): project = self.testdata_path + "test+project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % (urllib.quote(project), request) header, body = [str(_v) for _v in self.server.handleRequest(query_string)] self.assert_headers(header, body) response = header + body f = open(self.testdata_path + 'wfs_getfeature_' + requestid + '.txt') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) self.assertEqual(response, expected, msg=u"request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, unicode(expected, errors='replace'), unicode(response, errors='replace'))) def test_getfeature(self): tests = [] tests.append(('nobbox', u'GetFeature&TYPENAME=testlayer')) tests.append(('startindex2', u'GetFeature&TYPENAME=testlayer&STARTINDEX=2')) for id, req in tests: self.wfs_getfeature_compare(id, req) def assert_headers(self, header, body): headers = Message(StringIO(header)) if 'content-length' in headers: content_length = int(headers['content-length']) body_length = len(body) self.assertEqual(content_length, body_length, msg="Header reported content-length: %d Actual body length was: %d" % (content_length, body_length))
class TestQgsServerSecurity(unittest.TestCase): @classmethod def setUpClass(cls): cls.testdatapath = unitTestDataPath('qgis_server_security') + '/' cls.db = os.path.join(cls.testdatapath, 'db.sqlite') cls.db_clone = os.path.join(cls.testdatapath, 'db_clone.sqlite') cls.project = os.path.join(cls.testdatapath, 'project.qgs') cls.app = QgsApplication([], False) @classmethod def tearDownClass(cls): cls.app.exitQgis() try: os.remove(cls.db_clone) except OSError: pass def setUp(self): self.server = QgsServer() copyfile(self.db, self.db_clone) def test_wms_getfeatureinfo_filter_and_based_blind(self): """ And-based blind attack to check the kind of database currently used (if the result is valid for the point nammed 'b', then sqlite_version() function exist). But does not work because of the whitelist. If you remove the safety check, this is a valid injection. """ filter_sql = "point:\"name\" = 'b'" injection_sql = ") and (select sqlite_version()" query = "{0} {1}".format(filter_sql, injection_sql) d, h = self.handle_request_wms_getfeatureinfo(query) self.assertFalse(b"name = 'b'" in d) def test_wms_getfeatureinfo_filter_time_based_blind(self): """ Time-based blind to check the current version of database. If the server is too long to respond, then we have the answer! But it does not work because of the whitelist. If you remove the safety check, this is a valid injection. """ # first step, retrieve the version of sqlite by a regular way conn = spatialite_connect(self.db_clone) cur = conn.cursor() sql = "select sqlite_version()" sqlite_version = '' for row in cur.execute(sql): sqlite_version = row[0] conn.close() # second step, check the time of response for an invalid version filter_sql = "point:\"name\" = 'b'" injection_sql = ") and (select case sqlite_version() when '0.0.0' then substr(upper(hex(randomblob(99999999))),0,1) end)--" query = "{0} {1}".format(filter_sql, injection_sql) start = time.time() d, h = self.handle_request_wms_getfeatureinfo(query) duration_invalid_version = time.time() - start # third step, check the time of response for a valid version # maximum: several seconds injection_sql = ") and (select case sqlite_version() when '{0}' then substr(upper(hex(randomblob(99999999))),0,1) end)--".format( sqlite_version) query = "{0} {1}".format(filter_sql, injection_sql) start = time.time() d, h = self.handle_request_wms_getfeatureinfo(query) duration_valid_version = time.time() - start # compare duration. On my computer when safety check is deactivated: # duration_invalid_version: 0.012360334396362305 # duration_valid_version: 2.8810460567474365 self.assertAlmostEqual(duration_valid_version, duration_invalid_version, delta=0.5) def test_wms_getfeatureinfo_filter_stacked(self): """ The aim is to execute some staked queries. Here the 'drop' function is used but it could be done with create, insert, ... But the filter string is split thanks to the semicolon so it seems totally ignored whatever the query is (even without the safety check). """ filter_sql = "point:\"name\" = 'fake'" injection_sql = "); drop table point" query = "{0} {1}".format(filter_sql, injection_sql) d, h = self.handle_request_wms_getfeatureinfo(query) self.assertTrue(self.is_point_table_still_exist()) def test_wms_getfeatureinfo_filter_union_0(self): """ The aim is to retrieve name of tables within the database (like 'SpatialIndex'). But the whitelist blocks this request because of invalid tokens. If you remove the safety check, this is a valid injection. """ filter_sql = "point:\"name\" = 'fake'" injection_sql = ") union select 1,1,name,1,1 from sqlite_master where type = \"table\" order by name--" query = "{0} {1}".format(filter_sql, injection_sql) d, h = self.handle_request_wms_getfeatureinfo(query) self.assertFalse(b'SpatialIndex' in d) def test_wms_getfeatureinfo_filter_union_1(self): """ The aim is to retrieve data from an excluded layer. But the whitelist blocks this request because of invalid tokens. If you remove the safety check, this is a valid injection. """ filter_sql = "point:\"name\" = 'fake'" injection_sql = ") union select 1,1,* from aoi--" query = "{0} {1}".format(filter_sql, injection_sql) d, h = self.handle_request_wms_getfeatureinfo(query) self.assertFalse(b'private_value' in d) def test_wms_getfeatureinfo_filter_unicode(self): """ The aim is to send some invalid token in unicode to bypass the whitelist. But unicode is interpreted and checked by the safety function. """ # %3B -> semicolon filter_sql = "point:\"name\" = 'fake %3B'" d, h = self.handle_request_wms_getfeatureinfo(filter_sql) self.assertTrue(self.check_service_exception_report(d)) def test_wms_getfeatureinfo_filter_patternmatching(self): """ The aim is to retrieve the table's name thanks to pattern matching. If you remove the safety check, this is a valid injection. """ filter_sql = "point:\"name\" = 'b'" injection_sql = "or ( select name from sqlite_master where type='table' and name like '{0}') != ''" query = "{0} {1}".format(filter_sql, injection_sql) # there's no table named as 'az%' name = "az%" sql = query.format(name) d, h = self.handle_request_wms_getfeatureinfo(sql) # self.assertTrue(b"name = 'b'" in d) #true if sanity check deactivated self.assertTrue(self.check_service_exception_report(d)) # a table named as 'ao%' exist name = "ao%" sql = query.format(name) d, h = self.handle_request_wms_getfeatureinfo(sql) # self.assertTrue(b"name = 'a'" in d) #true if sanity check deactivated self.assertTrue(self.check_service_exception_report(d)) # a table named as 'aoi' exist name = "aoi" sql = query.format(name) d, h = self.handle_request_wms_getfeatureinfo(sql) # self.assertTrue(b"name = 'a'" in d) #true if sanity check deactivated self.assertTrue(self.check_service_exception_report(d)) def test_wms_getfeatureinfo_filter_whitelist(self): """ The aim is to check that some tokens cannot pass the safety check whatever their positions in the filter string. """ # create filter_sql = "point:\"name\" = 'a'create" d, h = self.handle_request_wms_getfeatureinfo(filter_sql) self.assertTrue(self.check_service_exception_report(d)) filter_sql = "point:\"name\" = 'a' create" d, h = self.handle_request_wms_getfeatureinfo(filter_sql) self.assertTrue(self.check_service_exception_report(d)) # invalid token and escape single quote filter_sql = "point:\"name\" = 'a\\'create" d, h = self.handle_request_wms_getfeatureinfo(filter_sql) self.assertTrue(self.check_service_exception_report(d)) # drop filter_sql = "point:\"name\" = 'a' drop" d, h = self.handle_request_wms_getfeatureinfo(filter_sql) self.assertTrue(self.check_service_exception_report(d)) # select filter_sql = "point:\"name\" = 'a' select" d, h = self.handle_request_wms_getfeatureinfo(filter_sql) self.assertTrue(self.check_service_exception_report(d)) # comments filter_sql = "point:\"name\" = 'a' #" d, h = self.handle_request_wms_getfeatureinfo(filter_sql) self.assertTrue(self.check_service_exception_report(d)) filter_sql = "point:\"name\" = 'a' -" d, h = self.handle_request_wms_getfeatureinfo(filter_sql) self.assertTrue(self.check_service_exception_report(d)) def test_wfs_getfeature_filter_stacked(self): """ The aim is to execute some staked queries within the 'Literal' and 'PropertyName' field. Here the 'drop' function is used but it could be done with create, insert, ... But due to the implementation, these filters are not resolved on database side but in server side with QgsExpression. So, there's no 'WHERE' clause and the query never reach the database. By the way, it's exactly the same thing whatever the kind of attacks and for the EXP_FILTER parameter too (filter described with QgsExpression). It's typically the kind of SQL injection which has been fixed in mapserver several years ago: https://trac.osgeo.org/mapserver/ticket/3874 """ # ogc:Literal / ogc:PropertyIsEqualTo literal = "4')); drop table point --" filter_xml = "<ogc:Filter%20xmlns:ogc=\"http://www.opengis.net/ogc\"><ogc:PropertyIsEqualTo><ogc:PropertyName>pkuid</ogc:PropertyName><ogc:Literal>{0}</ogc:Literal></ogc:PropertyIsEqualTo></ogc:Filter>".format( literal) self.handle_request_wfs_getfeature_filter(filter_xml) self.assertTrue(self.is_point_table_still_exist()) # ogc:Literal / ogc:PropertyIsLike literal = "4')); drop table point --" filter_xml = "<ogc:Filter%20xmlns:ogc=\"http://www.opengis.net/ogc\"><ogc:PropertyIsLike><ogc:PropertyName>pkuid</ogc:PropertyName><ogc:Literal>{0}</ogc:Literal></ogc:PropertyIsLike></ogc:Filter>".format( literal) self.handle_request_wfs_getfeature_filter(filter_xml) self.assertTrue(self.is_point_table_still_exist()) # ogc:PropertyName / ogc:PropertyIsLike propname = "name = 'a')); drop table point --" filter_xml = "<ogc:Filter%20xmlns:ogc=\"http://www.opengis.net/ogc\"><ogc:PropertyIsLike><ogc:PropertyName>{0}</ogc:PropertyName><ogc:Literal>4</ogc:Literal></ogc:PropertyIsLike></ogc:Filter>".format( propname) self.handle_request_wfs_getfeature_filter(filter_xml) self.assertTrue(self.is_point_table_still_exist()) def test_wms_getmap_sld_stacked(self): """ The aim is to execute some staked queries within the 'Literal' and 'PropertyName' field. Here the 'drop' function is used but it could be done with create, insert, ... However it's not working because special characters are duplicated. For example, with 'Literal' as "4')); drop table point --", the underlying query is: SELECT .... AND (("pkuid" = '4'')); drop table point --')) """ literal = "4')); drop table point --" sld = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><StyledLayerDescriptor xmlns=\"http://www.opengis.net/sld\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:ogc=\"http://www.opengis.net/ogc\" xsi:schemaLocation=\"http://www.opengis.net/sld http://schemas.opengis.net/sld/1.1.0/StyledLayerDescriptor.xsd\" version=\"1.1.0\" xmlns:se=\"http://www.opengis.net/se\" xmlns:xlink=\"http://www.w3.org/1999/xlink\"> <NamedLayer> <se:Name>point</se:Name> <UserStyle> <se:Name>point</se:Name> <se:FeatureTypeStyle> <se:Rule> <se:Name>Single symbol</se:Name> <ogc:Filter xmlns:ogc=\"http://www.opengis.net/ogc\"> <ogc:PropertyIsEqualTo> <ogc:PropertyName>pkuid</ogc:PropertyName> <ogc:Literal>{0}</ogc:Literal> </ogc:PropertyIsEqualTo> </ogc:Filter> <se:PointSymbolizer> <se:Graphic> <se:Mark> <se:WellKnownName>circle</se:WellKnownName> <se:Fill><se:SvgParameter name=\"fill\">5e86a1</se:SvgParameter></se:Fill><se:Stroke><se:SvgParameter name=\"stroke\">000000</se:SvgParameter></se:Stroke></se:Mark><se:Size>7</se:Size></se:Graphic></se:PointSymbolizer></se:Rule></se:FeatureTypeStyle></UserStyle></NamedLayer></StyledLayerDescriptor>".format( literal) self.handle_request_wms_getmap(sld) self.assertTrue(self.is_point_table_still_exist()) def check_service_exception_report(self, d): """ Return True if a ServiceExceptionReport is raised, False otherwise """ if b'<ServiceExceptionReport' in d: return True else: return False def handle_request_wfs_getfeature_filter(self, filter_xml): qs = "&".join([ "%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.project), "SERVICE": "WFS", "VERSION": "1.1.1", "REQUEST": "GetFeature", "TYPENAME": "point", "STYLES": "", "CRS": "EPSG:32613", "FILTER": filter_xml }.items()) ]) return self.server.handleRequest(qs) def handle_request_wms_getfeatureinfo(self, filter_sql): qs = "&".join([ "%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.project), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetFeatureInfo", "QUERY_LAYERS": "point", "LAYERS": "point", "STYLES": "", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "BBOX": "606171,4822867,612834,4827375", "CRS": "EPSG:32613", "FILTER": filter_sql }.items()) ]) return self._result(self.server.handleRequest(qs)) def handle_request_wms_getmap(self, sld): qs = "&".join([ "%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.project), "SERVICE": "WMS", "VERSION": "1.0.0", "REQUEST": "GetMap", "QUERY_LAYERS": "point", "LAYERS": "point", "STYLES": "", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "BBOX": "606171,4822867,612834,4827375", "CRS": "EPSG:32613", "SLD": sld }.items()) ]) return self._result(self.server.handleRequest(qs)) def is_point_table_still_exist(self): conn = spatialite_connect(self.db_clone) cur = conn.cursor() sql = "select * from point" point_table_exist = True try: cur.execute(sql) except: point_table_exist = False conn.close() return point_table_exist def _result(self, data): headers = {} for line in data[0].decode('UTF-8').split("\n"): if line != "": header = line.split(":") self.assertEqual(len(header), 2, line) headers[str(header[0])] = str(header[1]).strip() return data[1], headers
class TestQgsServer(unittest.TestCase): def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def assert_headers(self, header, body): headers = Message(StringIO(header)) if 'content-length' in headers: content_length = int(headers['content-length']) body_length = len(body) self.assertEqual(content_length, body_length, msg="Header reported content-length: %d Actual body length was: %d" % (content_length, body_length)) def test_destructor_segfaults(self): """Segfault on destructor?""" server = QgsServer() del server def test_multiple_servers(self): """Segfaults?""" for i in range(10): locals()["s%s" % i] = QgsServer() locals()["s%s" % i].handleRequest() def test_api(self): """Using an empty query string (returns an XML exception) we are going to test if headers and body are returned correctly""" # Test as a whole header, body = [str(_v) for _v in self.server.handleRequest()] response = header + body expected = 'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n' self.assertEqual(response, expected) expected = 'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n' self.assertEqual(header, expected) # Test body expected = '<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n' self.assertEqual(body, expected) def test_pluginfilters(self): """Test python plugins filters""" try: from qgis.server import QgsServerFilter except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return class SimpleHelloFilter(QgsServerFilter): def requestReady(self): QgsMessageLog.logMessage("SimpleHelloFilter.requestReady") def sendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.sendResponse") def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() QgsMessageLog.logMessage("SimpleHelloFilter.responseComplete") if params.get('SERVICE', '').upper() == 'SIMPLE': request.clearHeaders() request.setHeader('Content-type', 'text/plain') request.clearBody() request.appendBody('Hello from SimpleServer!') serverIface = self.server.serverInterface() filter = SimpleHelloFilter(serverIface) serverIface.registerFilter(filter, 100) # Get registered filters self.assertEqual(filter, serverIface.filters()[100][0]) # Register some more filters class Filter1(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter1!') class Filter2(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter2!') filter1 = Filter1(serverIface) filter2 = Filter2(serverIface) serverIface.registerFilter(filter1, 101) serverIface.registerFilter(filter2, 200) serverIface.registerFilter(filter2, 100) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [str(_v) for _v in self.server.handleRequest('service=simple')] response = header + body expected = 'Content-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Test that the bindings for complex type QgsServerFiltersMap are working filters = {100: [filter, filter2], 101: [filter1], 200: [filter2]} serverIface.setFilters(filters) self.assertTrue(filter in serverIface.filters()[100]) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [str(_v) for _v in self.server.handleRequest('service=simple')] response = header + body expected = 'Content-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # WMS tests def wms_request_compare(self, request, extra=None, reference_file=None): project = self.testdata_path + "test+project.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3&REQUEST=%s' % (urllib.quote(project), request) if extra is not None: query_string += extra header, body = [str(_v) for _v in self.server.handleRequest(query_string)] response = header + body f = open(self.testdata_path + (request.lower() if not reference_file else reference_file) + '.txt') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) # for older GDAL versions (<2.0), id field will be integer type if int(osgeo.gdal.VersionInfo()[:1]) < 2: expected = expected.replace('typeName="Integer64" precision="0" length="10" editType="TextEdit" type="qlonglong"', 'typeName="Integer" precision="0" length="10" editType="TextEdit" type="int"') self.assertEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected, response)) def test_project_wms(self): """Test some WMS request""" for request in ('GetCapabilities', 'GetProjectSettings'): self.wms_request_compare(request) # Test getfeatureinfo response self.wms_request_compare('GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + 'info_format=text%2Fhtml&transparent=true&' + 'width=600&height=400&srs=EPSG%3A3857&bbox=913190.6389747962%2C' + '5606005.488876367%2C913235.426296057%2C5606035.347090538&' + 'query_layers=testlayer%20%C3%A8%C3%A9&X=190&Y=320', 'wms_getfeatureinfo-text-html') # Test getfeatureinfo default info_format self.wms_request_compare('GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + 'transparent=true&' + 'width=600&height=400&srs=EPSG%3A3857&bbox=913190.6389747962%2C' + '5606005.488876367%2C913235.426296057%2C5606035.347090538&' + 'query_layers=testlayer%20%C3%A8%C3%A9&X=190&Y=320', 'wms_getfeatureinfo-text-plain') def wms_inspire_request_compare(self, request): """WMS INSPIRE tests""" project = self.testdata_path + "test+project_inspire.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3.0&REQUEST=%s' % (urllib.quote(project), request) header, body = [str(_v) for _v in self.server.handleRequest(query_string)] response = header + body f = open(self.testdata_path + request.lower() + '_inspire.txt') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) self.assertEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected, response)) def test_project_wms_inspire(self): """Test some WMS request""" for request in ('GetCapabilities',): self.wms_inspire_request_compare(request) # WFS tests def wfs_request_compare(self, request): project = self.testdata_path + "test+project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % (urllib.quote(project), request) header, body = [str(_v) for _v in self.server.handleRequest(query_string)] self.assert_headers(header, body) response = header + body f = open(self.testdata_path + 'wfs_' + request.lower() + '.txt') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) # for older GDAL versions (<2.0), id field will be integer type if int(osgeo.gdal.VersionInfo()[:1]) < 2: expected = expected.replace('<element type="long" name="id"/>', '<element type="integer" name="id"/>') self.assertEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected, response)) def test_project_wfs(self): """Test some WFS request""" for request in ('GetCapabilities', 'DescribeFeatureType'): self.wfs_request_compare(request) def wfs_getfeature_compare(self, requestid, request): project = self.testdata_path + "test+project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % (urllib.quote(project), request) header, body = [str(_v) for _v in self.server.handleRequest(query_string)] self.assert_headers(header, body) response = header + body f = open(self.testdata_path + 'wfs_getfeature_' + requestid + '.txt') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) self.assertEqual(response, expected, msg=u"request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, unicode(expected, errors='replace'), unicode(response, errors='replace'))) def test_getfeature(self): tests = [] tests.append(('nobbox', u'GetFeature&TYPENAME=testlayer')) tests.append(('startindex2', u'GetFeature&TYPENAME=testlayer&STARTINDEX=2')) for id, req in tests: self.wfs_getfeature_compare(id, req) def test_getLegendGraphics(self): """Test that does not return an exception but an image""" parms = { 'MAP': self.testdata_path + "test%2Bproject.qgs", 'SERVICE': 'WMS', 'VERSIONE': '1.0.0', 'REQUEST': 'GetLegendGraphic', 'FORMAT': 'image/png', #'WIDTH': '20', # optional #'HEIGHT': '20', # optional 'LAYER': u'testlayer+èé', } qs = '&'.join([u"%s=%s" % (k, v) for k, v in parms.iteritems()]) h, r = self.server.handleRequest(qs) self.assertEqual(-1, h.find('Content-Type: text/xml; charset=utf-8'), "Header: %s\nResponse:\n%s" % (h, r)) self.assertNotEquals(-1, h.find('Content-Type: image/png'), "Header: %s\nResponse:\n%s" % (h, r))
class QgsServerTestBase(unittest.TestCase): """Base class for QGIS server tests""" # Set to True in child classes to re-generate reference files for this class regenerate_reference = False def assertXMLEqual(self, response, expected, msg=''): """Compare XML line by line and sorted attributes""" response_lines = response.splitlines() expected_lines = expected.splitlines() line_no = 1 self.assertEqual(len(expected_lines), len(response_lines), "Expected and response have different number of lines!\n{}".format(msg)) for expected_line in expected_lines: expected_line = expected_line.strip() response_line = response_lines[line_no - 1].strip() response_line = response_line.replace(b'e+6', b'e+06') # Compare tag if re.match(RE_ELEMENT, expected_line): expected_elements = re.findall(RE_ELEMENT, expected_line) response_elements = re.findall(RE_ELEMENT, response_line) self.assertEqual(expected_elements[0], response_elements[0], msg=msg + "\nTag mismatch on line %s: %s != %s" % (line_no, expected_line, response_line)) # Compare content if len(expected_elements) == 2 and expected_elements[0] == expected_elements[1]: expected_element_content = re.findall(RE_ELEMENT_CONTENT, expected_line) response_element_content = re.findall(RE_ELEMENT_CONTENT, response_line) self.assertEqual(len(expected_element_content), len(response_element_content), msg=msg + "\nContent mismatch on line %s: %s != %s" % (line_no, expected_line, response_line)) if len(expected_element_content): self.assertEqual(expected_element_content[0], response_element_content[0], msg=msg + "\nContent mismatch on line %s: %s != %s" % (line_no, expected_line, response_line)) else: self.assertEqual(expected_line, response_line, msg=msg + "\nTag line mismatch %s: %s != %s\n%s" % (line_no, expected_line, response_line, msg)) # print("---->%s\t%s == %s" % (line_no, expected_line, response_line)) # Compare attributes if re.findall(RE_ATTRIBUTES, expected_line): # has attrs expected_attrs, expected_values = zip(*sorted(re.findall(RE_ATTRIBUTES, expected_line))) self.assertTrue(re.findall(RE_ATTRIBUTES, response_line), msg=msg + "\nXML attributes differ at line {0}: {1} != {2}".format(line_no, expected_line, response_line)) response_attrs, response_values = zip(*sorted(re.findall(RE_ATTRIBUTES, response_line))) self.assertEqual(expected_attrs, response_attrs, msg=msg + "\nXML attributes differ at line {0}: {1} != {2}".format(line_no, expected_attrs, response_attrs)) self.assertEqual(expected_values, response_values, msg=msg + "\nXML attribute values differ at line {0}: {1} != {2}".format(line_no, expected_values, response_values)) line_no += 1 @classmethod def setUpClass(cls): cls.app = QgsApplication([], False) @classmethod def tearDownClass(cls): cls.app.exitQgis() def setUp(self): """Create the server instance""" self.fontFamily = QgsFontUtils.standardTestFontFamily() QgsFontUtils.loadStandardTestFonts(['All']) self.testdata_path = unitTestDataPath('qgis_server') + '/' d = unitTestDataPath('qgis_server_accesscontrol') + '/' self.projectPath = os.path.join(d, "project.qgs") self.projectAnnotationPath = os.path.join(d, "project_with_annotations.qgs") self.projectStatePath = os.path.join(d, "project_state.qgs") self.projectUseLayerIdsPath = os.path.join(d, "project_use_layerids.qgs") self.projectGroupsPath = os.path.join(d, "project_groups.qgs") # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def strip_version_xmlns(self, text): """Order of attributes is random, strip version and xmlns""" return text.replace(b'version="1.3.0"', b'').replace(b'xmlns="http://www.opengis.net/ogc"', b'') def assert_headers(self, header, body): stream = StringIO() header_string = header.decode('utf-8') stream.write(header_string) headers = email.message_from_string(header_string) if 'content-length' in headers: content_length = int(headers['content-length']) body_length = len(body) self.assertEqual(content_length, body_length, msg="Header reported content-length: %d Actual body length was: %d" % (content_length, body_length)) @classmethod def store_reference(self, reference_path, response): """Utility to store reference files""" # Normally this is false if not self.regenerate_reference: return # Store the output for debug or to regenerate the reference documents: f = open(reference_path, 'wb+') f.write(response) f.close() def _result(self, data): headers = {} for line in data[0].decode('UTF-8').split("\n"): if line != "": header = line.split(":") self.assertEqual(len(header), 2, line) headers[str(header[0])] = str(header[1]).strip() return data[1], headers def _img_diff(self, image, control_image, max_diff, max_size_diff=QSize()): temp_image = os.path.join(tempfile.gettempdir(), "%s_result.png" % control_image) with open(temp_image, "wb") as f: f.write(image) control = QgsRenderChecker() control.setControlPathPrefix("qgis_server") control.setControlName(control_image) control.setRenderedImage(temp_image) if max_size_diff.isValid(): control.setSizeTolerance(max_size_diff.width(), max_size_diff.height()) return control.compareImages(control_image, max_diff), control.report() def _img_diff_error(self, response, headers, image, max_diff=100, max_size_diff=QSize()): self.assertEqual( headers.get("Content-Type"), "image/png", "Content type is wrong: %s\n%s" % (headers.get("Content-Type"), response)) test, report = self._img_diff(response, image, max_diff, max_size_diff) with open(os.path.join(tempfile.gettempdir(), image + "_result.png"), "rb") as rendered_file: encoded_rendered_file = base64.b64encode(rendered_file.read()) if not os.environ.get('ENCODED_OUTPUT'): message = "Image is wrong\: rendered file %s/%s_result.png" % (tempfile.gettempdir(), image) else: message = "Image is wrong\n%s\nImage:\necho '%s' | base64 -d >%s/%s_result.png" % ( report, encoded_rendered_file.strip().decode('utf8'), tempfile.gettempdir(), image ) # If the failure is in image sizes the diff file will not exists. if os.path.exists(os.path.join(tempfile.gettempdir(), image + "_result_diff.png")): with open(os.path.join(tempfile.gettempdir(), image + "_result_diff.png"), "rb") as diff_file: if not os.environ.get('ENCODED_OUTPUT'): message = "Image is wrong\: diff file %s/%s_result_diff.png" % (tempfile.gettempdir(), image) else: encoded_diff_file = base64.b64encode(diff_file.read()) message += "\nDiff:\necho '%s' | base64 -d > %s/%s_result_diff.png" % ( encoded_diff_file.strip().decode('utf8'), tempfile.gettempdir(), image ) self.assertTrue(test, message) def _execute_request(self, qs, requestMethod=QgsServerRequest.GetMethod, data=None): request = QgsBufferServerRequest(qs, requestMethod, {}, data) response = QgsBufferServerResponse() self.server.handleRequest(request, response) headers = [] rh = response.headers() rk = sorted(rh.keys()) for k in rk: headers.append(("%s: %s" % (k, rh[k])).encode('utf-8')) return b"\n".join(headers) + b"\n\n", bytes(response.body()) def _execute_request_project(self, qs, project, requestMethod=QgsServerRequest.GetMethod, data=None): request = QgsBufferServerRequest(qs, requestMethod, {}, data) response = QgsBufferServerResponse() self.server.handleRequest(request, response, project) headers = [] rh = response.headers() rk = sorted(rh.keys()) for k in rk: headers.append(("%s: %s" % (k, rh[k])).encode('utf-8')) return b"\n".join(headers) + b"\n\n", bytes(response.body())
attributes.remove("colour") return attributes def allowToEdit(self, layer, feature): """ Are we authorise to modify the following geometry """ if not self._active: return super(RestrictedAccessControl, self).allowToEdit(layer, feature) return feature.attribute("color") in ["red", "yellow"] def cacheKey(self): return "r" if self._active else "f" server = QgsServer() server.handleRequest() server_iface = server.serverInterface() accesscontrol = RestrictedAccessControl(server_iface) server_iface.registerAccessControl(accesscontrol, 100) class TestQgsServerAccessControl(TestCase): def setUp(self): self.testdata_path = unitTestDataPath("qgis_server_accesscontrol") dataFile = os.path.join(self.testdata_path, "helloworld.db") self.assertTrue(os.path.isfile(dataFile), 'Could not find data file "{}"'.format(dataFile)) copyfile(dataFile, os.path.join(self.testdata_path, "_helloworld.db"))
class TestQgsServerPlugins(QgsServerTestBase): def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' d = unitTestDataPath('qgis_server_accesscontrol') + '/' self.projectPath = os.path.join(d, "project.qgs") # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def test_pluginfilters(self): """Test python plugins filters""" try: from qgis.server import QgsServerFilter except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return class SimpleHelloFilter(QgsServerFilter): def requestReady(self): QgsMessageLog.logMessage("SimpleHelloFilter.requestReady") def sendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.sendResponse") def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() QgsMessageLog.logMessage("SimpleHelloFilter.responseComplete") if params.get('SERVICE', '').upper() == 'SIMPLE': request.clear() request.setResponseHeader('Content-type', 'text/plain') request.appendBody('Hello from SimpleServer!'.encode('utf-8')) serverIface = self.server.serverInterface() filter = SimpleHelloFilter(serverIface) serverIface.registerFilter(filter, 100) # Get registered filters self.assertEqual(filter, serverIface.filters()[100][0]) # global to be modified inside plugin filters globals()['status_code'] = 0 # body to be checked inside plugin filters globals()['body2'] = None # headers to be checked inside plugin filters globals()['headers2'] = None # Register some more filters class Filter1(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter1!'.encode('utf-8')) class Filter2(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter2!'.encode('utf-8')) class Filter3(QgsServerFilter): """Test get and set status code""" def responseComplete(self): global status_code request = self.serverInterface().requestHandler() request.setStatusCode(999) status_code = request.statusCode() class Filter4(QgsServerFilter): """Body getter""" def responseComplete(self): global body2 request = self.serverInterface().requestHandler() body2 = request.body() class Filter5(QgsServerFilter): """Body setter, clear body, keep headers""" def responseComplete(self): global headers2 request = self.serverInterface().requestHandler() request.clearBody() headers2 = request.responseHeaders() request.appendBody('new body, new life!'.encode('utf-8')) filter1 = Filter1(serverIface) filter2 = Filter2(serverIface) filter3 = Filter3(serverIface) filter4 = Filter4(serverIface) serverIface.registerFilter(filter1, 101) serverIface.registerFilter(filter2, 200) serverIface.registerFilter(filter2, 100) serverIface.registerFilter(filter3, 300) serverIface.registerFilter(filter4, 400) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [_v for _v in self._execute_request('?service=simple')] response = header + body expected = b'Content-Length: 62\nContent-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Check status code self.assertEqual(status_code, 999) # Check body getter from filter self.assertEqual(body2, b'Hello from SimpleServer!Hello from Filter1!Hello from Filter2!') # Check that the bindings for complex type QgsServerFiltersMap are working filters = {100: [filter, filter2], 101: [filter1], 200: [filter2]} serverIface.setFilters(filters) self.assertTrue(filter in serverIface.filters()[100]) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [_v for _v in self._execute_request('?service=simple')] response = header + body expected = b'Content-Length: 62\nContent-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Now, re-run with body setter filter5 = Filter5(serverIface) serverIface.registerFilter(filter5, 500) header, body = [_v for _v in self._execute_request('?service=simple')] response = header + body expected = b'Content-Length: 19\nContent-type: text/plain\n\nnew body, new life!' self.assertEqual(response, expected) self.assertEqual(headers2, {'Content-type': 'text/plain'})
class TestQgsServer(unittest.TestCase): def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def test_destructor_segfaults(self): """Segfault on destructor?""" server = QgsServer() del server def test_multiple_servers(self): """Segfaults?""" for i in range(10): locals()["s%s" % i] = QgsServer() locals()["s%s" % i].handleRequest() def test_api(self): """Using an empty query string (returns an XML exception) we are going to test if headers and body are returned correctly""" # Test as a whole response = str(self.server.handleRequest()) expected = 'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n' self.assertEqual(response, expected) # Test header response = str(self.server.handleRequestGetHeaders()) expected = 'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n' self.assertEqual(response, expected) # Test body response = str(self.server.handleRequestGetBody()) expected = '<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n' self.assertEqual(response, expected) def test_pluginfilters(self): """Test python plugins filters""" try: from qgis.server import QgsServerFilter except ImportError: print "QGIS Server plugins are not compiled. Skipping test" return class SimpleHelloFilter(QgsServerFilter): def requestReady(self): QgsMessageLog.logMessage("SimpleHelloFilter.requestReady") def sendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.sendResponse") def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() QgsMessageLog.logMessage("SimpleHelloFilter.responseComplete") if params.get('SERVICE', '').upper() == 'SIMPLE': request.clearHeaders() request.setHeader('Content-type', 'text/plain') request.clearBody() request.appendBody('Hello from SimpleServer!') serverIface = self.server.serverInterface() serverIface.registerFilter(SimpleHelloFilter(serverIface), 100 ) response = str(self.server.handleRequest('service=simple')) expected = 'Content-type: text/plain\n\nHello from SimpleServer!' self.assertEqual(response, expected) ## WMS tests def wms_request_compare(self, request): map = self.testdata_path + "testproject.qgs" query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3&REQUEST=%s' % (map, request) response = str(self.server.handleRequest(query_string)) f = open(self.testdata_path + request.lower() + '.txt') expected = f.read() f.close() # Store for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) self.assertEqual(response, expected, msg="request %s failed. Expected:\n%s\n\nResponse:\n%s" % (request, expected, response)) def test_project_wms(self): """Test some WMS request""" for request in ('GetCapabilities', 'GetProjectSettings'): self.wms_request_compare(request)
def test_multiple_servers(self): """Segfaults?""" for i in range(10): locals()["s%s" % i] = QgsServer() locals()["s%s" % i].handleRequest()
class TestQgsServer(unittest.TestCase): def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def test_destructor_segfaults(self): """Segfault on destructor?""" server = QgsServer() del server def test_multiple_servers(self): """Segfaults?""" for i in range(10): locals()["s%s" % i] = QgsServer() locals()["s%s" % i].handleRequest() def test_api(self): """Using an empty query string (returns an XML exception) we are going to test if headers and body are returned correctly""" # Test as a whole header, body = [str(_v) for _v in self.server.handleRequest()] response = header + body expected = 'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n' self.assertEqual(response, expected) expected = 'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n' self.assertEqual(header, expected) # Test body expected = '<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n' self.assertEqual(body, expected) def test_pluginfilters(self): """Test python plugins filters""" try: from qgis.server import QgsServerFilter except ImportError: print "QGIS Server plugins are not compiled. Skipping test" return class SimpleHelloFilter(QgsServerFilter): def requestReady(self): QgsMessageLog.logMessage("SimpleHelloFilter.requestReady") def sendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.sendResponse") def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() QgsMessageLog.logMessage("SimpleHelloFilter.responseComplete") if params.get('SERVICE', '').upper() == 'SIMPLE': request.clearHeaders() request.setHeader('Content-type', 'text/plain') request.clearBody() request.appendBody('Hello from SimpleServer!') serverIface = self.server.serverInterface() filter = SimpleHelloFilter(serverIface) serverIface.registerFilter(filter, 100) # Get registered filters self.assertEqual(filter, serverIface.filters()[100][0]) # Register some more filters class Filter1(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter1!') class Filter2(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter2!') filter1 = Filter1(serverIface) filter2 = Filter2(serverIface) serverIface.registerFilter(filter1, 101) serverIface.registerFilter(filter2, 200) serverIface.registerFilter(filter2, 100) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [ str(_v) for _v in self.server.handleRequest('service=simple') ] response = header + body expected = 'Content-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Test that the bindings for complex type QgsServerFiltersMap are working filters = {100: [filter, filter2], 101: [filter1], 200: [filter2]} serverIface.setFilters(filters) self.assertTrue(filter in serverIface.filters()[100]) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [ str(_v) for _v in self.server.handleRequest('service=simple') ] response = header + body expected = 'Content-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # WMS tests def wms_request_compare(self, request): project = self.testdata_path + "test+project.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3&REQUEST=%s' % ( urllib.quote(project), request) header, body = [ str(_v) for _v in self.server.handleRequest(query_string) ] response = header + body f = open(self.testdata_path + request.lower() + '.txt') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) self.assertEqual( response, expected, msg= "request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected, response)) def test_project_wms(self): """Test some WMS request""" for request in ('GetCapabilities', 'GetProjectSettings'): self.wms_request_compare(request) # WFS tests def wfs_request_compare(self, request): project = self.testdata_path + "test+project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % ( urllib.quote(project), request) header, body = [ str(_v) for _v in self.server.handleRequest(query_string) ] self.assert_headers(header, body) response = header + body f = open(self.testdata_path + 'wfs_' + request.lower() + '.txt') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) self.assertEqual( response, expected, msg= "request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected, response)) def test_project_wfs(self): """Test some WMS request""" for request in ('GetCapabilities', 'DescribeFeatureType'): self.wfs_request_compare(request) def wfs_getfeature_compare(self, requestid, request): project = self.testdata_path + "test+project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % ( urllib.quote(project), request) header, body = [ str(_v) for _v in self.server.handleRequest(query_string) ] self.assert_headers(header, body) response = header + body f = open(self.testdata_path + 'wfs_getfeature_' + requestid + '.txt') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) self.assertEqual( response, expected, msg= u"request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, unicode(expected, errors='replace'), unicode(response, errors='replace'))) def test_getfeature(self): tests = [] tests.append(('nobbox', u'GetFeature&TYPENAME=testlayer')) tests.append( ('startindex2', u'GetFeature&TYPENAME=testlayer&STARTINDEX=2')) for id, req in tests: self.wfs_getfeature_compare(id, req) def assert_headers(self, header, body): headers = Message(StringIO(header)) if 'content-length' in headers: content_length = int(headers['content-length']) body_length = len(body) self.assertEqual( content_length, body_length, msg= "Header reported content-length: %d Actual body length was: %d" % (content_length, body_length))
standard_library.install_aliases() __author__ = 'Alessandro Pasotti' __date__ = '05/15/2016' __copyright__ = 'Copyright 2016, The QGIS Project' # This will get replaced with a git SHA1 when you do a git archive __revision__ = '$Format:%H$' QGIS_SERVER_PORT = int(os.environ.get('QGIS_SERVER_PORT', '8083')) #QGIS_SERVER_HOST = os.environ.get('QGIS_SERVER_HOST', '127.0.0.1') QGIS_SERVER_HOST = '' # listen an all interfaces qgs_server = QgsServer() QGIS_SERVER_OAUTH2_USERNAME = os.environ.get( 'QGIS_SERVER_OAUTH2_USERNAME', 'username') QGIS_SERVER_OAUTH2_PASSWORD = os.environ.get( 'QGIS_SERVER_OAUTH2_PASSWORD', 'password') QGIS_SERVER_OAUTH2_TOKEN_EXPIRES_IN = os.environ.get( 'QGIS_SERVER_OAUTH2_TOKEN_EXPIRES_IN', 3600) print("SERVER_NAME (from env, will be used fo urls): %s" % os.environ.get('SERVER_NAME')) print("OAuth2 username: %s" % QGIS_SERVER_OAUTH2_USERNAME) print("OAuth2 password: %s" % QGIS_SERVER_OAUTH2_PASSWORD) # Naive token storage implementation
class QgsServerTestBase(unittest.TestCase): """Base class for QGIS server tests""" # Set to True in child classes to re-generate reference files for this class regenerate_reference = False def assertXMLEqual(self, response, expected, msg='', raw=False): """Compare XML line by line and sorted attributes""" response_lines = response.splitlines() expected_lines = expected.splitlines() line_no = 1 diffs = [] for diff in difflib.unified_diff( [l.decode('utf8') for l in expected_lines], [l.decode('utf8') for l in response_lines]): diffs.append(diff) self.assertEqual( len(expected_lines), len(response_lines), "Expected and response have different number of lines!\n{}\n{}\nWe got :\n{}" .format(msg, '\n'.join(diffs), '\n'.join([i.decode("utf-8") for i in response_lines]))) for expected_line in expected_lines: expected_line = expected_line.strip() response_line = response_lines[line_no - 1].strip() response_line = response_line.replace(b'e+6', br'e+06') # Compare tag if re.match(RE_ELEMENT, expected_line) and not raw: expected_elements = re.findall(RE_ELEMENT, expected_line) response_elements = re.findall(RE_ELEMENT, response_line) self.assertEqual(expected_elements[0], response_elements[0], msg=msg + "\nTag mismatch on line %s: %s != %s" % (line_no, expected_line, response_line)) # Compare content if len(expected_elements ) == 2 and expected_elements[0] == expected_elements[1]: expected_element_content = re.findall( RE_ELEMENT_CONTENT, expected_line) response_element_content = re.findall( RE_ELEMENT_CONTENT, response_line) self.assertEqual( len(expected_element_content), len(response_element_content), msg=msg + "\nContent mismatch on line %s: %s != %s" % (line_no, expected_line, response_line)) if len(expected_element_content): self.assertEqual( expected_element_content[0], response_element_content[0], msg=msg + "\nContent mismatch on line %s: %s != %s" % (line_no, expected_line, response_line)) else: self.assertEqual(expected_line, response_line, msg=msg + "\nTag line mismatch %s: %s != %s\n%s" % (line_no, expected_line, response_line, msg)) # print("---->%s\t%s == %s" % (line_no, expected_line, response_line)) # Compare attributes if re.findall(RE_ATTRIBUTES, expected_line): # has attrs expected_attrs, expected_values = zip( *sorted(re.findall(RE_ATTRIBUTES, expected_line))) self.assertTrue( re.findall(RE_ATTRIBUTES, response_line), msg=msg + "\nXML attributes differ at line {0}: {1} != {2}".format( line_no, expected_line, response_line)) response_attrs, response_values = zip( *sorted(re.findall(RE_ATTRIBUTES, response_line))) self.assertEqual( expected_attrs, response_attrs, msg=msg + "\nXML attributes differ at line {0}: {1} != {2}".format( line_no, expected_attrs, response_attrs)) self.assertEqual( expected_values, response_values, msg=msg + "\nXML attribute values differ at line {0}: {1} != {2}". format(line_no, expected_values, response_values)) line_no += 1 def setUp(self): """Create the server instance""" self.fontFamily = QgsFontUtils.standardTestFontFamily() QgsFontUtils.loadStandardTestFonts(['All']) self.testdata_path = unitTestDataPath('qgis_server') + '/' d = unitTestDataPath('qgis_server_accesscontrol') + '/' self.projectPath = os.path.join(d, "project.qgs") self.projectAnnotationPath = os.path.join( d, "project_with_annotations.qgs") self.projectStatePath = os.path.join(d, "project_state.qgs") self.projectUseLayerIdsPath = os.path.join(d, "project_use_layerids.qgs") self.projectGroupsPath = os.path.join(d, "project_groups.qgs") # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() # Disable landing page API to test standard legacy XML responses in case of errors os.environ["QGIS_SERVER_DISABLED_APIS"] = "Landing Page" def tearDown(self): """Cleanup env""" super().tearDown() try: del os.environ["QGIS_SERVER_DISABLED_APIS"] except KeyError: pass def strip_version_xmlns(self, text): """Order of attributes is random, strip version and xmlns""" return text.replace(b'version="1.3.0"', b'').replace(b'xmlns="http://www.opengis.net/ogc"', b'') def assert_headers(self, header, body): stream = StringIO() header_string = header.decode('utf-8') stream.write(header_string) headers = email.message_from_string(header_string) if 'content-length' in headers: content_length = int(headers['content-length']) body_length = len(body) self.assertEqual( content_length, body_length, msg= "Header reported content-length: %d Actual body length was: %d" % (content_length, body_length)) @classmethod def store_reference(self, reference_path, response): """Utility to store reference files""" # Normally this is false if not self.regenerate_reference: return # Store the output for debug or to regenerate the reference documents: f = open(reference_path, 'wb+') f.write(response) f.close() def _result(self, data): headers = {} for line in data[0].decode('UTF-8').split("\n"): if line != "": header = line.split(":") self.assertEqual(len(header), 2, line) headers[str(header[0])] = str(header[1]).strip() return data[1], headers def _img_diff(self, image, control_image, max_diff, max_size_diff=QSize(), outputFormat='PNG'): if outputFormat == 'PNG': extFile = 'png' elif outputFormat == 'JPG': extFile = 'jpg' elif outputFormat == 'WEBP': extFile = 'webp' else: raise RuntimeError('Yeah, new format implemented') temp_image = os.path.join(tempfile.gettempdir(), "%s_result.%s" % (control_image, extFile)) with open(temp_image, "wb") as f: f.write(image) if outputFormat != 'PNG': return (True, "QgsRenderChecker can only be used for PNG") control = QgsMultiRenderChecker() control.setControlPathPrefix("qgis_server") control.setControlName(control_image) control.setRenderedImage(temp_image) if max_size_diff.isValid(): control.setSizeTolerance(max_size_diff.width(), max_size_diff.height()) return control.runTest(control_image, max_diff), control.report() def _img_diff_error(self, response, headers, image, max_diff=100, max_size_diff=QSize(), unittest_data_path='control_images', outputFormat='PNG'): """ :param outputFormat: PNG, JPG or WEBP """ if outputFormat == 'PNG': extFile = 'png' contentType = 'image/png' elif outputFormat == 'JPG': extFile = 'jpg' contentType = 'image/jpeg' elif outputFormat == 'WEBP': extFile = 'webp' contentType = 'image/webp' else: raise RuntimeError('Yeah, new format implemented') reference_path = unitTestDataPath( unittest_data_path ) + '/qgis_server/' + image + '/' + image + '.' + extFile self.store_reference(reference_path, response) self.assertEqual( headers.get("Content-Type"), contentType, "Content type is wrong: %s instead of %s\n%s" % (headers.get("Content-Type"), contentType, response)) test, report = self._img_diff(response, image, max_diff, max_size_diff, outputFormat) with open( os.path.join(tempfile.gettempdir(), image + "_result." + extFile), "rb") as rendered_file: encoded_rendered_file = base64.b64encode(rendered_file.read()) if not os.environ.get('ENCODED_OUTPUT'): message = "Image is wrong: rendered file %s/%s_result.%s" % ( tempfile.gettempdir(), image, extFile) else: message = "Image is wrong\n%s\nImage:\necho '%s' | base64 -d >%s/%s_result.%s" % ( report, encoded_rendered_file.strip().decode('utf8'), tempfile.gettempdir(), image, extFile) # If the failure is in image sizes the diff file will not exists. if os.path.exists( os.path.join(tempfile.gettempdir(), image + "_result_diff." + extFile)): with open( os.path.join(tempfile.gettempdir(), image + "_result_diff." + extFile), "rb") as diff_file: if not os.environ.get('ENCODED_OUTPUT'): message = "Image is wrong: diff file %s/%s_result_diff.%s" % ( tempfile.gettempdir(), image, extFile) else: encoded_diff_file = base64.b64encode(diff_file.read()) message += "\nDiff:\necho '%s' | base64 -d > %s/%s_result_diff.%s" % ( encoded_diff_file.strip().decode('utf8'), tempfile.gettempdir(), image, extFile) self.assertTrue(test, message) def _execute_request(self, qs, requestMethod=QgsServerRequest.GetMethod, data=None, request_headers=None): request = QgsBufferServerRequest(qs, requestMethod, request_headers or {}, data) response = QgsBufferServerResponse() self.server.handleRequest(request, response) headers = [] rh = response.headers() rk = sorted(rh.keys()) for k in rk: headers.append(("%s: %s" % (k, rh[k])).encode('utf-8')) return b"\n".join(headers) + b"\n\n", bytes(response.body()) def _execute_request_project(self, qs, project, requestMethod=QgsServerRequest.GetMethod, data=None): request = QgsBufferServerRequest(qs, requestMethod, {}, data) response = QgsBufferServerResponse() self.server.handleRequest(request, response, project) headers = [] rh = response.headers() rk = sorted(rh.keys()) for k in rk: headers.append(("%s: %s" % (k, rh[k])).encode('utf-8')) return b"\n".join(headers) + b"\n\n", bytes(response.body()) def _assert_status_code(self, status_code, qs, requestMethod=QgsServerRequest.GetMethod, data=None, project=None): request = QgsBufferServerRequest(qs, requestMethod, {}, data) response = QgsBufferServerResponse() self.server.handleRequest(request, response, project) assert response.statusCode() == status_code, "%s != %s" % ( response.statusCode(), status_code) def _assertRed(self, color: QColor): self.assertEqual(color.red(), 255) self.assertEqual(color.green(), 0) self.assertEqual(color.blue(), 0) def _assertGreen(self, color: QColor): self.assertEqual(color.red(), 0) self.assertEqual(color.green(), 255) self.assertEqual(color.blue(), 0) def _assertBlue(self, color: QColor): self.assertEqual(color.red(), 0) self.assertEqual(color.green(), 0) self.assertEqual(color.blue(), 255) def _assertBlack(self, color: QColor): self.assertEqual(color.red(), 0) self.assertEqual(color.green(), 0) self.assertEqual(color.blue(), 255) def _assertWhite(self, color: QColor): self.assertEqual(color.red(), 255) self.assertEqual(color.green(), 255) self.assertEqual(color.blue(), 255)
class TestQgsServerPlugins(QgsServerTestBase): def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' d = unitTestDataPath('qgis_server_accesscontrol') + '/' self.projectPath = os.path.join(d, "project.qgs") # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def test_pluginfilters(self): """Test python plugins filters""" try: from qgis.server import QgsServerFilter except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return class SimpleHelloFilter(QgsServerFilter): def requestReady(self): QgsMessageLog.logMessage("SimpleHelloFilter.requestReady") def sendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.sendResponse") def onSendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.onSendResponse") def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() QgsMessageLog.logMessage("SimpleHelloFilter.responseComplete") if params.get('SERVICE', '').upper() == 'SIMPLE': request.clear() request.setResponseHeader('Content-type', 'text/plain') request.appendBody( 'Hello from SimpleServer!'.encode('utf-8')) serverIface = self.server.serverInterface() filter = SimpleHelloFilter(serverIface) serverIface.registerFilter(filter, 100) # Get registered filters self.assertEqual(filter, serverIface.filters()[100][0]) # global to be modified inside plugin filters globals()['status_code'] = 0 # body to be checked inside plugin filters globals()['body2'] = None # headers to be checked inside plugin filters globals()['headers2'] = None # Register some more filters class Filter1(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter1!'.encode('utf-8')) class Filter2(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter2!'.encode('utf-8')) class Filter3(QgsServerFilter): """Test get and set status code""" def responseComplete(self): global status_code request = self.serverInterface().requestHandler() request.setStatusCode(999) status_code = request.statusCode() class Filter4(QgsServerFilter): """Body getter""" def responseComplete(self): global body2 request = self.serverInterface().requestHandler() body2 = request.body() class Filter5(QgsServerFilter): """Body setter, clear body, keep headers""" def responseComplete(self): global headers2 request = self.serverInterface().requestHandler() request.clearBody() headers2 = request.responseHeaders() request.appendBody('new body, new life!'.encode('utf-8')) filter1 = Filter1(serverIface) filter2 = Filter2(serverIface) filter3 = Filter3(serverIface) filter4 = Filter4(serverIface) serverIface.registerFilter(filter1, 101) serverIface.registerFilter(filter2, 200) serverIface.registerFilter(filter2, 100) serverIface.registerFilter(filter3, 300) serverIface.registerFilter(filter4, 400) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [_v for _v in self._execute_request('?service=simple')] response = header + body expected = b'Content-Length: 62\nContent-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Check status code self.assertEqual(status_code, 999) # Check body getter from filter self.assertEqual( body2, b'Hello from SimpleServer!Hello from Filter1!Hello from Filter2!') # Check that the bindings for complex type QgsServerFiltersMap are working filters = {100: [filter, filter2], 101: [filter1], 200: [filter2]} serverIface.setFilters(filters) self.assertTrue(filter in serverIface.filters()[100]) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = self._execute_request('?service=simple') response = header + body expected = b'Content-Length: 62\nContent-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Now, re-run with body setter filter5 = Filter5(serverIface) serverIface.registerFilter(filter5, 500) header, body = self._execute_request('?service=simple') response = header + body expected = b'Content-Length: 19\nContent-type: text/plain\n\nnew body, new life!' self.assertEqual(response, expected) self.assertEqual(headers2, {'Content-type': 'text/plain'}) def test_configpath(self): """ Test plugin can read confif path """ try: from qgis.server import QgsServerFilter from qgis.core import QgsProject except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return d = unitTestDataPath('qgis_server_accesscontrol') + '/' self.projectPath = os.path.join(d, "project.qgs") self.server = QgsServer() # global to be modified inside plugin filters globals()['configFilePath2'] = None class Filter0(QgsServerFilter): """Body setter, clear body, keep headers""" def requestReady(self): global configFilePath2 configFilePath2 = self.serverInterface().configFilePath() serverIface = self.server.serverInterface() serverIface.registerFilter(Filter0(serverIface), 100) # Test using MAP self._execute_request('?service=simple&MAP=%s' % self.projectPath) # Check config file path self.assertEqual(configFilePath2, self.projectPath) # Reset result globals()['configFilePath2'] = None # Test with prqject as argument project = QgsProject() project.read(self.projectPath) self._execute_request_project('?service=simple', project=project) # Check config file path self.assertEqual(configFilePath2, project.fileName()) def test_exceptions(self): """Test that plugin filter Python exceptions can be caught""" try: from qgis.server import QgsServerFilter except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return class FilterBroken(QgsServerFilter): def responseComplete(self): raise Exception("There was something very wrong!") serverIface = self.server.serverInterface() filter1 = FilterBroken(serverIface) filters = {100: [filter1]} serverIface.setFilters(filters) header, body = self._execute_request('') self.assertEqual(body, b'Internal Server Error') serverIface.setFilters({}) def test_get_path(self): """Test get url and path from plugins""" try: from qgis.server import QgsServerFilter except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return class Filter1(QgsServerFilter): def responseComplete(self): handler = self.serverInterface().requestHandler() self.url = handler.url() self.path = handler.path() serverIface = self.server.serverInterface() filter1 = Filter1(serverIface) filters = {100: [filter1]} serverIface.setFilters(filters) header, body = self._execute_request( 'http://myserver/mypath/?myparam=1') self.assertEqual(filter1.url, 'http://myserver/mypath/?myparam=1') self.assertEqual(filter1.path, '/mypath/') serverIface.setFilters({}) def test_streaming_pipeline(self): """ Test streaming pipeline propagation """ try: from qgis.server import QgsServerFilter from qgis.core import QgsProject except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return # create a service for streaming data class StreamedService(QgsService): def __init__(self): super().__init__() self._response = b"Should never appear" self._name = "TestStreamedService" self._version = "1.0" def name(self): return self._name def version(self): return self._version def executeRequest(self, request, response, project): response.setStatusCode(206) response.write(self._response) response.flush() class Filter1(QgsServerFilter): def onRequestReady(self): request = self.serverInterface().requestHandler() return self.propagate def onSendResponse(self): request = self.serverInterface().requestHandler() request.clearBody() request.appendBody(b'A') request.sendResponse() request.appendBody(b'B') request.sendResponse() # Stop propagating return self.propagate def onResponseComplete(self): request = self.serverInterface().requestHandler() request.appendBody(b'C') return self.propagate # Methods should be called only if filter1 propagate class Filter2(QgsServerFilter): def __init__(self, iface): super().__init__(iface) self.request_ready = False def onRequestReady(self): request = self.serverInterface().requestHandler() self.request_ready = True return True def onSendResponse(self): request = self.serverInterface().requestHandler() request.appendBody(b'D') return True def onResponseComplete(self): request = self.serverInterface().requestHandler() request.appendBody(b'E') return True serverIface = self.server.serverInterface() serverIface.setFilters({}) service0 = StreamedService() reg = serverIface.serviceRegistry() reg.registerService(service0) filter1 = Filter1(serverIface) filter2 = Filter2(serverIface) serverIface.registerFilter(filter1, 200) serverIface.registerFilter(filter2, 300) project = QgsProject() # Test no propagation filter1.propagate = False _, body = self._execute_request_project('?service=%s' % service0.name(), project=project) self.assertFalse(filter2.request_ready) self.assertEqual(body, b'ABC') # Test with propagation filter1.propagate = True _, body = self._execute_request_project('?service=%s' % service0.name(), project=project) self.assertTrue(filter2.request_ready) self.assertEqual(body, b'ABDCE') serverIface.setFilters({}) reg.unregisterService(service0.name(), service0.version())
class TestQgsServer(unittest.TestCase): def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def assert_headers(self, header, body): headers = Message(StringIO(header)) if 'content-length' in headers: content_length = int(headers['content-length']) body_length = len(body) self.assertEqual(content_length, body_length, msg="Header reported content-length: %d Actual body length was: %d" % (content_length, body_length)) def test_destructor_segfaults(self): """Segfault on destructor?""" server = QgsServer() del server def test_multiple_servers(self): """Segfaults?""" for i in range(10): locals()["s%s" % i] = QgsServer() locals()["s%s" % i].handleRequest() def test_api(self): """Using an empty query string (returns an XML exception) we are going to test if headers and body are returned correctly""" # Test as a whole header, body = [str(_v) for _v in self.server.handleRequest()] response = header + body expected = 'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n' self.assertEqual(response, expected) expected = 'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n' self.assertEqual(header, expected) # Test body expected = '<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n' self.assertEqual(body, expected) def test_pluginfilters(self): """Test python plugins filters""" try: from qgis.server import QgsServerFilter except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return class SimpleHelloFilter(QgsServerFilter): def requestReady(self): QgsMessageLog.logMessage("SimpleHelloFilter.requestReady") def sendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.sendResponse") def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() QgsMessageLog.logMessage("SimpleHelloFilter.responseComplete") if params.get('SERVICE', '').upper() == 'SIMPLE': request.clearHeaders() request.setHeader('Content-type', 'text/plain') request.clearBody() request.appendBody('Hello from SimpleServer!') serverIface = self.server.serverInterface() filter = SimpleHelloFilter(serverIface) serverIface.registerFilter(filter, 100) # Get registered filters self.assertEqual(filter, serverIface.filters()[100][0]) # Register some more filters class Filter1(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter1!') class Filter2(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter2!') filter1 = Filter1(serverIface) filter2 = Filter2(serverIface) serverIface.registerFilter(filter1, 101) serverIface.registerFilter(filter2, 200) serverIface.registerFilter(filter2, 100) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [str(_v) for _v in self.server.handleRequest('service=simple')] response = header + body expected = 'Content-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Test that the bindings for complex type QgsServerFiltersMap are working filters = {100: [filter, filter2], 101: [filter1], 200: [filter2]} serverIface.setFilters(filters) self.assertTrue(filter in serverIface.filters()[100]) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [str(_v) for _v in self.server.handleRequest('service=simple')] response = header + body expected = 'Content-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # WMS tests def wms_request_compare(self, request, extra=None, reference_file=None): project = self.testdata_path + "test+project.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3&REQUEST=%s' % (urllib.quote(project), request) if extra is not None: query_string += extra header, body = [str(_v) for _v in self.server.handleRequest(query_string)] response = header + body f = open(self.testdata_path + (request.lower() if not reference_file else reference_file) + '.txt') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() #""" response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) # for older GDAL versions (<2.0), id field will be integer type if int(osgeo.gdal.VersionInfo()[:1]) < 2: expected = expected.replace('typeName="Integer64" precision="0" length="10" editType="TextEdit" type="qlonglong"', 'typeName="Integer" precision="0" length="10" editType="TextEdit" type="int"') self.assertEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected, response)) def test_project_wms(self): """Test some WMS request""" for request in ('GetCapabilities', 'GetProjectSettings'): self.wms_request_compare(request) # Test getfeatureinfo response self.wms_request_compare('GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + 'info_format=text%2Fhtml&transparent=true&' + 'width=600&height=400&srs=EPSG%3A3857&bbox=913190.6389747962%2C' + '5606005.488876367%2C913235.426296057%2C5606035.347090538&' + 'query_layers=testlayer%20%C3%A8%C3%A9&X=190&Y=320', 'wms_getfeatureinfo-text-html') # Test getfeatureinfo default info_format self.wms_request_compare('GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + 'transparent=true&' + 'width=600&height=400&srs=EPSG%3A3857&bbox=913190.6389747962%2C' + '5606005.488876367%2C913235.426296057%2C5606035.347090538&' + 'query_layers=testlayer%20%C3%A8%C3%A9&X=190&Y=320', 'wms_getfeatureinfo-text-plain') # Regression for #8656 # Mind the gap! (the space in the FILTER expression) self.wms_request_compare('GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&' + 'INFO_FORMAT=text%2Fxml&' + 'width=600&height=400&srs=EPSG%3A3857&' + 'query_layers=testlayer%20%C3%A8%C3%A9&' + 'FEATURE_COUNT=10&FILTER=testlayer%20%C3%A8%C3%A9' + urllib.quote(':"NAME" = \'two\''), 'wms_getfeatureinfo_filter') def wms_inspire_request_compare(self, request): """WMS INSPIRE tests""" project = self.testdata_path + "test+project_inspire.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3.0&REQUEST=%s' % (urllib.quote(project), request) header, body = [str(_v) for _v in self.server.handleRequest(query_string)] response = header + body f = open(self.testdata_path + request.lower() + '_inspire.txt') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) self.assertEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected, response)) def test_project_wms_inspire(self): """Test some WMS request""" for request in ('GetCapabilities',): self.wms_inspire_request_compare(request) # WFS tests def wfs_request_compare(self, request): project = self.testdata_path + "test+project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % (urllib.quote(project), request) header, body = [str(_v) for _v in self.server.handleRequest(query_string)] self.assert_headers(header, body) response = header + body f = open(self.testdata_path + 'wfs_' + request.lower() + '.txt') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) # for older GDAL versions (<2.0), id field will be integer type if int(osgeo.gdal.VersionInfo()[:1]) < 2: expected = expected.replace('<element type="long" name="id"/>', '<element type="integer" name="id"/>') self.assertEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected, response)) def test_project_wfs(self): """Test some WFS request""" for request in ('GetCapabilities', 'DescribeFeatureType'): self.wfs_request_compare(request) def wfs_getfeature_compare(self, requestid, request): project = self.testdata_path + "test+project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % (urllib.quote(project), request) header, body = [str(_v) for _v in self.server.handleRequest(query_string)] self.result_compare( 'wfs_getfeature_' + requestid + '.txt', u"request %s failed.\n Query: %s" % ( query_string, request, ), header, body ) def result_compare(self, file_name, error_msg_header, header, body): self.assert_headers(header, body) response = header + body f = open(self.testdata_path + file_name) expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_PATH, '', response) expected = re.sub(RE_STRIP_PATH, '', expected) self.assertEqual(response, expected, msg=u"%s\n Expected:\n%s\n\n Response:\n%s" % (error_msg_header, unicode(expected, errors='replace'), unicode(response, errors='replace'))) def test_getfeature(self): tests = [] tests.append(('nobbox', u'GetFeature&TYPENAME=testlayer')) tests.append(('startindex2', u'GetFeature&TYPENAME=testlayer&STARTINDEX=2')) tests.append(('limit2', u'GetFeature&TYPENAME=testlayer&MAXFEATURES=2')) tests.append(('start1_limit1', u'GetFeature&TYPENAME=testlayer&MAXFEATURES=1&STARTINDEX=1')) for id, req in tests: self.wfs_getfeature_compare(id, req) def wfs_getfeature_post_compare(self, requestid, request): project = self.testdata_path + "test+project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP={}'.format(urllib.quote(project)) self.server.putenv("REQUEST_METHOD", "POST") self.server.putenv("REQUEST_BODY", request) header, body = self.server.handleRequest(query_string) self.server.putenv("REQUEST_METHOD", '') self.server.putenv("REQUEST_BODY", '') self.result_compare( 'wfs_getfeature_{}.txt'.format(requestid), "GetFeature in POST for '{}' failed.".format(requestid), header, body, ) def test_getfeature_post(self): template = """<?xml version="1.0" encoding="UTF-8"?> <wfs:GetFeature service="WFS" version="1.0.0" {} xmlns:wfs="http://www.opengis.net/wfs" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs http://schemas.opengis.net/wfs/1.1.0/wfs.xsd"> <wfs:Query typeName="testlayer" xmlns:feature="http://www.qgis.org/gml"> <ogc:Filter xmlns:ogc="http://www.opengis.net/ogc"> <ogc:BBOX> <ogc:PropertyName>geometry</ogc:PropertyName> <gml:Envelope xmlns:gml="http://www.opengis.net/gml"> <gml:lowerCorner>8 44</gml:lowerCorner> <gml:upperCorner>9 45</gml:upperCorner> </gml:Envelope> </ogc:BBOX> </ogc:Filter> </wfs:Query> </wfs:GetFeature> """ tests = [] tests.append(('nobbox', template.format(""))) tests.append(('startindex2', template.format('startIndex="2"'))) tests.append(('limit2', template.format('maxFeatures="2"'))) tests.append(('start1_limit1', template.format('startIndex="1" maxFeatures="1"'))) for id, req in tests: self.wfs_getfeature_post_compare(id, req) def test_getLegendGraphics(self): """Test that does not return an exception but an image""" parms = { 'MAP': self.testdata_path + "test%2Bproject.qgs", 'SERVICE': 'WMS', 'VERSION': '1.0.0', 'REQUEST': 'GetLegendGraphic', 'FORMAT': 'image/png', #'WIDTH': '20', # optional #'HEIGHT': '20', # optional 'LAYER': u'testlayer+èé', } qs = '&'.join([u"%s=%s" % (k, v) for k, v in parms.iteritems()]) h, r = self.server.handleRequest(qs) self.assertEqual(-1, h.find('Content-Type: text/xml; charset=utf-8'), "Header: %s\nResponse:\n%s" % (h, r)) self.assertNotEquals(-1, h.find('Content-Type: image/png'), "Header: %s\nResponse:\n%s" % (h, r))
class TestQgsServer(unittest.TestCase): def assertXMLEqual(self, response, expected, msg=''): """Compare XML line by line and sorted attributes""" response_lines = response.splitlines() expected_lines = expected.splitlines() line_no = 1 for expected_line in expected_lines: expected_line = expected_line.strip() response_line = response_lines[line_no - 1].strip() # Compare tag try: self.assertEqual( re.findall(b'<([^>\s]+)[ >]', expected_line)[0], re.findall(b'<([^>\s]+)[ >]', response_line)[0], msg=msg + "\nTag mismatch on line %s: %s != %s" % (line_no, expected_line, response_line)) except IndexError: self.assertEqual(expected_line, response_line, msg=msg + "\nTag line mismatch %s: %s != %s" % (line_no, expected_line, response_line)) #print("---->%s\t%s == %s" % (line_no, expected_line, response_line)) # Compare attributes if re.match(RE_ATTRIBUTES, expected_line): # has attrs expected_attrs = sorted( re.findall(RE_ATTRIBUTES, expected_line)) response_attrs = sorted( re.findall(RE_ATTRIBUTES, response_line)) self.assertEqual( expected_attrs, response_attrs, msg=msg + "\nXML attributes differ at line {0}: {1} != {2}".format( line_no, expected_attrs, response_attrs)) line_no += 1 @classmethod def setUpClass(cls): cls.app = QgsApplication([], False) @classmethod def tearDownClass(cls): cls.app.exitQgis() def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' d = unitTestDataPath('qgis_server_accesscontrol') + '/' self.projectPath = os.path.join(d, "project.qgs") # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def strip_version_xmlns(self, text): """Order of attributes is random, strip version and xmlns""" return text.replace(b'version="1.3.0"', b'').replace(b'xmlns="http://www.opengis.net/ogc"', b'') def assert_headers(self, header, body): stream = StringIO() header_string = header.decode('utf-8') stream.write(header_string) headers = email.message_from_string(header_string) if 'content-length' in headers: content_length = int(headers['content-length']) body_length = len(body) self.assertEqual( content_length, body_length, msg= "Header reported content-length: %d Actual body length was: %d" % (content_length, body_length)) def test_destructor_segfaults(self): """Segfault on destructor?""" server = QgsServer() del server def test_multiple_servers(self): """Segfaults?""" for i in range(10): locals()["s%s" % i] = QgsServer() locals()["s%s" % i].handleRequest() def test_api(self): """Using an empty query string (returns an XML exception) we are going to test if headers and body are returned correctly""" # Test as a whole header, body = [_v for _v in self.server.handleRequest()] response = self.strip_version_xmlns(header + body) expected = self.strip_version_xmlns( b'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n' ) self.assertEqual(response, expected) expected = b'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n' self.assertEqual(header, expected) # Test body expected = self.strip_version_xmlns( b'<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n' ) self.assertEqual(self.strip_version_xmlns(body), expected) def test_pluginfilters(self): """Test python plugins filters""" try: from qgis.server import QgsServerFilter except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return class SimpleHelloFilter(QgsServerFilter): def requestReady(self): QgsMessageLog.logMessage("SimpleHelloFilter.requestReady") def sendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.sendResponse") def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() QgsMessageLog.logMessage("SimpleHelloFilter.responseComplete") if params.get('SERVICE', '').upper() == 'SIMPLE': request.clearHeaders() request.setHeader('Content-type', 'text/plain') request.clearBody() request.appendBody( 'Hello from SimpleServer!'.encode('utf-8')) serverIface = self.server.serverInterface() filter = SimpleHelloFilter(serverIface) serverIface.registerFilter(filter, 100) # Get registered filters self.assertEqual(filter, serverIface.filters()[100][0]) # Register some more filters class Filter1(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter1!'.encode('utf-8')) class Filter2(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter2!'.encode('utf-8')) filter1 = Filter1(serverIface) filter2 = Filter2(serverIface) serverIface.registerFilter(filter1, 101) serverIface.registerFilter(filter2, 200) serverIface.registerFilter(filter2, 100) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [ _v for _v in self.server.handleRequest('service=simple') ] response = header + body expected = b'Content-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Test that the bindings for complex type QgsServerFiltersMap are working filters = {100: [filter, filter2], 101: [filter1], 200: [filter2]} serverIface.setFilters(filters) self.assertTrue(filter in serverIface.filters()[100]) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [ _v for _v in self.server.handleRequest('service=simple') ] response = header + body expected = b'Content-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # WMS tests def wms_request_compare(self, request, extra=None, reference_file=None): project = self.testdata_path + "test_project.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3&REQUEST=%s' % ( urllib.parse.quote(project), request) if extra is not None: query_string += extra header, body = self.server.handleRequest(query_string) response = header + body reference_path = self.testdata_path + ( request.lower() if not reference_file else reference_file) + '.txt' f = open(reference_path, 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(reference_path, 'wb+') f.write(response) f.close() f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() #""" response = re.sub(RE_STRIP_UNCHECKABLE, b'*****', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'*****', expected) # for older GDAL versions (<2.0), id field will be integer type if int(osgeo.gdal.VersionInfo()[:1]) < 2: expected = expected.replace( b'typeName="Integer64" precision="0" length="10" editType="TextEdit" type="qlonglong"', b'typeName="Integer" precision="0" length="10" editType="TextEdit" type="int"' ) self.assertXMLEqual( response, expected, msg= "request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected.decode('utf-8'), response.decode('utf-8'))) def test_project_wms(self): """Test some WMS request""" for request in ('GetCapabilities', 'GetProjectSettings'): self.wms_request_compare(request) # Test getfeatureinfo response self.wms_request_compare( 'GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + 'info_format=text%2Fhtml&transparent=true&' + 'width=600&height=400&srs=EPSG%3A3857&bbox=913190.6389747962%2C' + '5606005.488876367%2C913235.426296057%2C5606035.347090538&' + 'query_layers=testlayer%20%C3%A8%C3%A9&X=190&Y=320', 'wms_getfeatureinfo-text-html') # Test getfeatureinfo default info_format self.wms_request_compare( 'GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + 'transparent=true&' + 'width=600&height=400&srs=EPSG%3A3857&bbox=913190.6389747962%2C' + '5606005.488876367%2C913235.426296057%2C5606035.347090538&' + 'query_layers=testlayer%20%C3%A8%C3%A9&X=190&Y=320', 'wms_getfeatureinfo-text-plain') # Regression for #8656 # Mind the gap! (the space in the FILTER expression) self.wms_request_compare( 'GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&' + 'INFO_FORMAT=text%2Fxml&' + 'width=600&height=400&srs=EPSG%3A3857&' + 'query_layers=testlayer%20%C3%A8%C3%A9&' + 'FEATURE_COUNT=10&FILTER=testlayer%20%C3%A8%C3%A9' + urllib.parse.quote(':"NAME" = \'two\''), 'wms_getfeatureinfo_filter') def wms_inspire_request_compare(self, request): """WMS INSPIRE tests""" project = self.testdata_path + "test_project_inspire.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3.0&REQUEST=%s' % ( urllib.parse.quote(project), request) header, body = self.server.handleRequest(query_string) response = header + body f = open(self.testdata_path + request.lower() + '_inspire.txt', 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_UNCHECKABLE, b'', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'', expected) self.assertXMLEqual( response, expected, msg= "request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected.decode('utf-8'), response.decode('utf-8'))) def test_project_wms_inspire(self): """Test some WMS request""" for request in ('GetCapabilities', ): self.wms_inspire_request_compare(request) # WFS tests def wfs_request_compare(self, request): project = self.testdata_path + "test_project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % ( urllib.parse.quote(project), request) header, body = self.server.handleRequest(query_string) self.assert_headers(header, body) response = header + body f = open(self.testdata_path + 'wfs_' + request.lower() + '.txt', 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_UNCHECKABLE, b'', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'', expected) # for older GDAL versions (<2.0), id field will be integer type if int(osgeo.gdal.VersionInfo()[:1]) < 2: expected = expected.replace( b'<element type="long" name="id"/>', b'<element type="integer" name="id"/>') self.assertXMLEqual( response, expected, msg= "request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected.decode('utf-8'), response.decode('utf-8'))) def test_project_wfs(self): """Test some WFS request""" for request in ('GetCapabilities', 'DescribeFeatureType'): self.wfs_request_compare(request) def wfs_getfeature_compare(self, requestid, request): project = self.testdata_path + "test_project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % ( urllib.parse.quote(project), request) header, body = self.server.handleRequest(query_string) self.result_compare( 'wfs_getfeature_' + requestid + '.txt', "request %s failed.\n Query: %s" % ( query_string, request, ), header, body) def result_compare(self, file_name, error_msg_header, header, body): self.assert_headers(header, body) response = header + body f = open(self.testdata_path + file_name, 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_UNCHECKABLE, b'', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'', expected) self.assertXMLEqual(response, expected, msg="%s\n Expected:\n%s\n\n Response:\n%s" % (error_msg_header, str(expected, errors='replace'), str(response, errors='replace'))) def test_getfeature(self): tests = [] tests.append(('nobbox', 'GetFeature&TYPENAME=testlayer')) tests.append( ('startindex2', 'GetFeature&TYPENAME=testlayer&STARTINDEX=2')) tests.append(('limit2', 'GetFeature&TYPENAME=testlayer&MAXFEATURES=2')) tests.append( ('start1_limit1', 'GetFeature&TYPENAME=testlayer&MAXFEATURES=1&STARTINDEX=1')) for id, req in tests: self.wfs_getfeature_compare(id, req) def wfs_getfeature_post_compare(self, requestid, request): project = self.testdata_path + "test_project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP={}'.format(urllib.parse.quote(project)) self.server.putenv("REQUEST_METHOD", "POST") self.server.putenv("REQUEST_BODY", request) header, body = self.server.handleRequest(query_string) self.server.putenv("REQUEST_METHOD", '') self.server.putenv("REQUEST_BODY", '') self.result_compare( 'wfs_getfeature_{}.txt'.format(requestid), "GetFeature in POST for '{}' failed.".format(requestid), header, body, ) def test_getfeature_post(self): template = """<?xml version="1.0" encoding="UTF-8"?> <wfs:GetFeature service="WFS" version="1.0.0" {} xmlns:wfs="http://www.opengis.net/wfs" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs http://schemas.opengis.net/wfs/1.1.0/wfs.xsd"> <wfs:Query typeName="testlayer" xmlns:feature="http://www.qgis.org/gml"> <ogc:Filter xmlns:ogc="http://www.opengis.net/ogc"> <ogc:BBOX> <ogc:PropertyName>geometry</ogc:PropertyName> <gml:Envelope xmlns:gml="http://www.opengis.net/gml"> <gml:lowerCorner>8 44</gml:lowerCorner> <gml:upperCorner>9 45</gml:upperCorner> </gml:Envelope> </ogc:BBOX> </ogc:Filter> </wfs:Query> </wfs:GetFeature> """ tests = [] tests.append(('nobbox', template.format(""))) tests.append(('startindex2', template.format('startIndex="2"'))) tests.append(('limit2', template.format('maxFeatures="2"'))) tests.append(('start1_limit1', template.format('startIndex="1" maxFeatures="1"'))) for id, req in tests: self.wfs_getfeature_post_compare(id, req) def test_wms_getmap_order(self): qs = "&".join([ "%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Hello,Country", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items()) ]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_LayerOrder") def test_wms_getmap_srs(self): qs = "&".join([ "%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country,Hello", "STYLES": "", "FORMAT": "image/png", "BBOX": "-151.7,-38.9,51.0,78.0", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:4326" }.items()) ]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_SRS") def test_wms_getmap_style(self): # default style qs = "&".join([ "%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country_Labels", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items()) ]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_StyleDefault") # custom style qs = "&".join([ "%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country_Labels", "STYLES": "custom", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items()) ]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_StyleCustom") def test_wms_getmap_filter(self): qs = "&".join([ "%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country,Hello", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "FILTER": "Country:\"name\" = 'eurasia'" }.items()) ]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_Filter") def test_wms_getmap_selection(self): qs = "&".join([ "%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country,Hello", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "SRS": "EPSG:3857", "SELECTION": "Country_Labels: 4" }.items()) ]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_Selection") def test_wms_getmap_opacities(self): qs = "&".join([ "%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetMap", "LAYERS": "Country,Hello", "STYLES": "", "FORMAT": "image/png", "BBOX": "-16817707,-4710778,5696513,14587125", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857", "OPACITIES": "125, 50" }.items()) ]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetMap_Opacities") def test_wms_getprint_basic(self): qs = "&".join([ "%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetPrint", "TEMPLATE": "layoutA4", "FORMAT": "png", "map0:EXTENT": "-33626185.498,-13032965.185,33978427.737,16020257.031", "map0:LAYERS": "Country,Hello", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items()) ]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetPrint_Basic") def test_wms_getprint_srs(self): qs = "&".join([ "%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetPrint", "TEMPLATE": "layoutA4", "FORMAT": "png", "map0:EXTENT": "-309.015,-133.011,312.179,133.949", "map0:LAYERS": "Country,Hello", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:4326" }.items()) ]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetPrint_SRS") def test_wms_getprint_scale(self): qs = "&".join([ "%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetPrint", "TEMPLATE": "layoutA4", "FORMAT": "png", "map0:EXTENT": "-33626185.498,-13032965.185,33978427.737,16020257.031", "map0:LAYERS": "Country,Hello", "map0:SCALE": "36293562", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items()) ]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetPrint_Scale") def test_wms_getprint_grid(self): qs = "&".join([ "%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetPrint", "TEMPLATE": "layoutA4", "FORMAT": "png", "map0:EXTENT": "-33626185.498,-13032965.185,33978427.737,16020257.031", "map0:LAYERS": "Country,Hello", "map0:GRID_INTERVAL_X": "1000000", "map0:GRID_INTERVAL_Y": "2000000", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items()) ]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetPrint_Grid") def test_wms_getprint_rotation(self): qs = "&".join([ "%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetPrint", "TEMPLATE": "layoutA4", "FORMAT": "png", "map0:EXTENT": "-33626185.498,-13032965.185,33978427.737,16020257.031", "map0:LAYERS": "Country,Hello", "map0:ROTATION": "45", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items()) ]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetPrint_Rotation") def test_getLegendGraphics(self): """Test that does not return an exception but an image""" parms = { 'MAP': self.testdata_path + "test_project.qgs", 'SERVICE': 'WMS', 'VERSION': '1.3.0', 'REQUEST': 'GetLegendGraphic', 'FORMAT': 'image/png', #'WIDTH': '20', # optional #'HEIGHT': '20', # optional 'LAYER': 'testlayer%20èé', } qs = '&'.join(["%s=%s" % (k, v) for k, v in parms.items()]) h, r = self.server.handleRequest(qs) self.assertEqual(-1, h.find(b'Content-Type: text/xml; charset=utf-8'), "Header: %s\nResponse:\n%s" % (h, r)) self.assertNotEqual(-1, h.find(b'Content-Type: image/png'), "Header: %s\nResponse:\n%s" % (h, r)) def test_getLegendGraphics_layertitle(self): """Test that does not return an exception but an image""" parms = { 'MAP': self.testdata_path + "test_project.qgs", 'SERVICE': 'WMS', 'VERSION': '1.3.0', 'REQUEST': 'GetLegendGraphic', 'FORMAT': 'image/png', #'WIDTH': '20', # optional #'HEIGHT': '20', # optional 'LAYER': u'testlayer%20èé', 'LAYERTITLE': 'TRUE', } qs = '&'.join([u"%s=%s" % (k, v) for k, v in parms.items()]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_test", 250, QSize(10, 10)) parms = { 'MAP': self.testdata_path + "test_project.qgs", 'SERVICE': 'WMS', 'VERSION': '1.3.0', 'REQUEST': 'GetLegendGraphic', 'FORMAT': 'image/png', #'WIDTH': '20', # optional #'HEIGHT': '20', # optional 'LAYER': u'testlayer%20èé', 'LAYERTITLE': 'FALSE', } qs = '&'.join([u"%s=%s" % (k, v) for k, v in parms.items()]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_test_layertitle_false", 250, QSize(10, 10)) def test_wms_GetLegendGraphic_Basic(self): qs = "&".join([ "%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items()) ]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_Basic") def test_wms_GetLegendGraphic_BoxSpace(self): qs = "&".join([ "%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "BOXSPACE": "100", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items()) ]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_BoxSpace") def test_wms_GetLegendGraphic_SymbolSpace(self): qs = "&".join([ "%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "SYMBOLSPACE": "100", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items()) ]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_SymbolSpace") def test_wms_GetLegendGraphic_IconLabelSpace(self): qs = "&".join([ "%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "ICONLABELSPACE": "100", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items()) ]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_IconLabelSpace") def test_wms_GetLegendGraphic_SymbolSize(self): qs = "&".join([ "%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello", "LAYERTITLE": "FALSE", "SYMBOLWIDTH": "50", "SYMBOLHEIGHT": "30", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "CRS": "EPSG:3857" }.items()) ]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_SymbolSize") def test_wms_GetLegendGraphic_BBox(self): qs = "&".join([ "%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello,db_point", "LAYERTITLE": "FALSE", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "BBOX": "-151.7,-38.9,51.0,78.0", "CRS": "EPSG:4326" }.items()) ]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_BBox") def test_wms_GetLegendGraphic_BBox2(self): qs = "&".join([ "%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetLegendGraphic", "LAYER": "Country,Hello,db_point", "LAYERTITLE": "FALSE", "FORMAT": "image/png", "HEIGHT": "500", "WIDTH": "500", "BBOX": "-76.08,-6.4,-19.38,38.04", "SRS": "EPSG:4326" }.items()) ]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_BBox2") def _result(self, data): headers = {} for line in data[0].decode('UTF-8').split("\n"): if line != "": header = line.split(":") self.assertEqual(len(header), 2, line) headers[str(header[0])] = str(header[1]).strip() return data[1], headers def _img_diff(self, image, control_image, max_diff, max_size_diff=QSize()): temp_image = os.path.join(tempfile.gettempdir(), "%s_result.png" % control_image) with open(temp_image, "wb") as f: f.write(image) control = QgsRenderChecker() control.setControlPathPrefix("qgis_server") control.setControlName(control_image) control.setRenderedImage(temp_image) if max_size_diff.isValid(): control.setSizeTolerance(max_size_diff.width(), max_size_diff.height()) return control.compareImages(control_image), control.report() def _img_diff_error(self, response, headers, image, max_diff=10, max_size_diff=QSize()): self.assertEqual( headers.get("Content-Type"), "image/png", "Content type is wrong: %s" % headers.get("Content-Type")) test, report = self._img_diff(response, image, max_diff, max_size_diff) with open(os.path.join(tempfile.gettempdir(), image + "_result.png"), "rb") as rendered_file: encoded_rendered_file = base64.b64encode(rendered_file.read()) message = "Image is wrong\n%s\nImage:\necho '%s' | base64 -d >%s/%s_result.png" % ( report, encoded_rendered_file.strip(), tempfile.gettempdir(), image) # If the failure is in image sizes the diff file will not exists. if os.path.exists( os.path.join(tempfile.gettempdir(), image + "_result_diff.png")): with open( os.path.join(tempfile.gettempdir(), image + "_result_diff.png"), "rb") as diff_file: encoded_diff_file = base64.b64encode(diff_file.read()) message += "\nDiff:\necho '%s' | base64 -d > %s/%s_result_diff.png" % ( encoded_diff_file.strip(), tempfile.gettempdir(), image) self.assertTrue(test, message)
QGIS_SERVER_PKI_CERTIFICATE = os.environ.get('QGIS_SERVER_PKI_CERTIFICATE') QGIS_SERVER_PKI_KEY = os.environ.get('QGIS_SERVER_PKI_KEY') QGIS_SERVER_PKI_AUTHORITY = os.environ.get('QGIS_SERVER_PKI_AUTHORITY') QGIS_SERVER_PKI_USERNAME = os.environ.get('QGIS_SERVER_PKI_USERNAME') # Check if PKI - https is enabled https = (QGIS_SERVER_PKI_CERTIFICATE is not None and os.path.isfile(QGIS_SERVER_PKI_CERTIFICATE) and QGIS_SERVER_PKI_KEY is not None and os.path.isfile(QGIS_SERVER_PKI_KEY) and QGIS_SERVER_PKI_AUTHORITY is not None and os.path.isfile(QGIS_SERVER_PKI_AUTHORITY) and QGIS_SERVER_PKI_USERNAME) qgs_app = QgsApplication([], False) qgs_server = QgsServer() if os.environ.get('QGIS_SERVER_HTTP_BASIC_AUTH') is not None: from qgis.server import QgsServerFilter import base64 class HTTPBasicFilter(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() if self.serverInterface().getEnv('HTTP_AUTHORIZATION'): username, password = base64.b64decode( self.serverInterface().getEnv( 'HTTP_AUTHORIZATION')[6:]).split(b':') if (username.decode('utf-8') == os.environ.get( 'QGIS_SERVER_USERNAME', 'username') and password.decode('utf-8') == os.environ.get(
from qgis.core import QgsMessageLog from qgis.server import QgsServer, QgsServerFilter standard_library.install_aliases() __author__ = 'Alessandro Pasotti' __date__ = '05/15/2016' __copyright__ = 'Copyright 2016, The QGIS Project' # This will get replaced with a git SHA1 when you do a git archive __revision__ = '$Format:%H$' QGIS_SERVER_PORT = int(os.environ.get('QGIS_SERVER_PORT', '8083')) #QGIS_SERVER_HOST = os.environ.get('QGIS_SERVER_HOST', '127.0.0.1') QGIS_SERVER_HOST = '' # listen an all interfaces qgs_server = QgsServer() QGIS_SERVER_OAUTH2_USERNAME = os.environ.get('QGIS_SERVER_OAUTH2_USERNAME', 'username') QGIS_SERVER_OAUTH2_PASSWORD = os.environ.get('QGIS_SERVER_OAUTH2_PASSWORD', 'password') QGIS_SERVER_OAUTH2_TOKEN_EXPIRES_IN = os.environ.get( 'QGIS_SERVER_OAUTH2_TOKEN_EXPIRES_IN', 3600) print("SERVER_NAME (from env, will be used fo urls): %s" % os.environ.get('SERVER_NAME')) print("OAuth2 username: %s" % QGIS_SERVER_OAUTH2_USERNAME) print("OAuth2 password: %s" % QGIS_SERVER_OAUTH2_PASSWORD) # Naive token storage implementation _tokens = {}
import urllib.parse from http.server import BaseHTTPRequestHandler, HTTPServer from qgis.server import QgsServer try: QGIS_SERVER_DEFAULT_PORT = int(os.environ['QGIS_SERVER_DEFAULT_PORT']) except KeyError: QGIS_SERVER_DEFAULT_PORT = 8081 try: QGIS_SERVER_DEFAULT_SERVERNAME = os.environ['QGIS_SERVER_DEFAULT_SERVERNAME'] except KeyError: QGIS_SERVER_DEFAULT_SERVERNAME = 'localhost' qgs_server = QgsServer() # OAuth 2 plugin loading start serverIface = qgs_server.serverInterface() from oauth_settings import * import importlib module = importlib.import_module('filters.%s' % OAUTH2_AUTHORIZATION_SERVICE_PROVIDER) klass_name = 'OAuth2Filter%s' % OAUTH2_AUTHORIZATION_SERVICE_PROVIDER.title() klass = getattr(module, klass_name) serverIface.registerFilter(klass(serverIface), 100) # OAuth 2 plugin loading End class Handler(BaseHTTPRequestHandler): def __init__(self, request, client_address, server):
attributes.remove("colour") return attributes def allowToEdit(self, layer, feature): """ Are we authorise to modify the following geometry """ if not self._active: return super(RestrictedAccessControl, self).allowToEdit(layer, feature) return feature.attribute("color") in ["red", "yellow"] def cacheKey(self): return "r" if self._active else "f" server = QgsServer() server.handleRequest() server_iface = server.serverInterface() accesscontrol = RestrictedAccessControl(server_iface) server_iface.registerAccessControl(accesscontrol, 100) class TestQgsServerAccessControl(unittest.TestCase): def setUp(self): self.testdata_path = unitTestDataPath("qgis_server_accesscontrol") dataFile = os.path.join(self.testdata_path, "helloworld.db") self.assertTrue(os.path.isfile(dataFile), 'Could not find data file "{}"'.format(dataFile)) copyfile(dataFile, os.path.join(self.testdata_path, "_helloworld.db"))
# Check if OAuth2 is enabled QGIS_SERVER_OAUTH2_AUTH = ( QGIS_SERVER_OAUTH2_CERTIFICATE is not None and os.path.isfile(QGIS_SERVER_OAUTH2_CERTIFICATE) and QGIS_SERVER_OAUTH2_KEY is not None and os.path.isfile(QGIS_SERVER_OAUTH2_KEY) and QGIS_SERVER_OAUTH2_AUTHORITY is not None and os.path.isfile(QGIS_SERVER_OAUTH2_AUTHORITY) and QGIS_SERVER_OAUTH2_USERNAME and QGIS_SERVER_OAUTH2_PASSWORD) HTTPS_ENABLED = QGIS_SERVER_PKI_AUTH or QGIS_SERVER_OAUTH2_AUTH qgs_app = QgsApplication([], False) qgs_server = QgsServer() if QGIS_SERVER_HTTP_BASIC_AUTH: from qgis.server import QgsServerFilter import base64 class HTTPBasicFilter(QgsServerFilter): def requestReady(self): handler = self.serverInterface().requestHandler() auth = self.serverInterface().requestHandler().requestHeader('HTTP_AUTHORIZATION') if auth: username, password = base64.b64decode(auth[6:]).split(b':') if (username.decode('utf-8') == os.environ.get('QGIS_SERVER_USERNAME', 'username') and password.decode('utf-8') == os.environ.get('QGIS_SERVER_PASSWORD', 'password')):
def test_destructor_segfaults(self): """Segfault on destructor?""" server = QgsServer() del server
class TestQgsServer(unittest.TestCase): def assertXMLEqual(self, response, expected, msg=''): """Compare XML line by line and sorted attributes""" response_lines = response.splitlines() expected_lines = expected.splitlines() line_no = 1 for expected_line in expected_lines: expected_line = expected_line.strip() response_line = response_lines[line_no - 1].strip() # Compare tag try: self.assertEqual(re.findall(b'<([^>\s]+)[ >]', expected_line)[0], re.findall(b'<([^>\s]+)[ >]', response_line)[0], msg=msg + "\nTag mismatch on line %s: %s != %s" % (line_no, expected_line, response_line)) except IndexError: self.assertEqual(expected_line, response_line, msg=msg + "\nTag line mismatch %s: %s != %s" % (line_no, expected_line, response_line)) #print("---->%s\t%s == %s" % (line_no, expected_line, response_line)) # Compare attributes if re.match(RE_ATTRIBUTES, expected_line): # has attrs expected_attrs = re.findall(RE_ATTRIBUTES, expected_line) expected_attrs.sort() response_attrs = re.findall(RE_ATTRIBUTES, response_line) response_attrs.sort() self.assertEqual(expected_attrs, response_attrs, msg=msg + "\nXML attributes differ at line {0}: {1} != {2}".format(line_no, expected_attrs, response_attrs)) line_no += 1 @classmethod def setUpClass(cls): cls.app = QgsApplication([], False) @classmethod def tearDownClass(cls): cls.app.exitQgis() def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def strip_version_xmlns(self, text): """Order of attributes is random, strip version and xmlns""" return text.replace(b'version="1.3.0"', b'').replace(b'xmlns="http://www.opengis.net/ogc"', b'') def assert_headers(self, header, body): stream = StringIO() header_string = header.decode('utf-8') stream.write(header_string) headers = email.message_from_string(header_string) if 'content-length' in headers: content_length = int(headers['content-length']) body_length = len(body) self.assertEqual(content_length, body_length, msg="Header reported content-length: %d Actual body length was: %d" % (content_length, body_length)) def test_destructor_segfaults(self): """Segfault on destructor?""" server = QgsServer() del server def test_multiple_servers(self): """Segfaults?""" for i in range(10): locals()["s%s" % i] = QgsServer() locals()["s%s" % i].handleRequest() def test_api(self): """Using an empty query string (returns an XML exception) we are going to test if headers and body are returned correctly""" # Test as a whole header, body = [_v for _v in self.server.handleRequest()] response = self.strip_version_xmlns(header + body) expected = self.strip_version_xmlns(b'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n') self.assertEqual(response, expected) expected = b'Content-Length: 206\nContent-Type: text/xml; charset=utf-8\n\n' self.assertEqual(header, expected) # Test body expected = self.strip_version_xmlns(b'<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">\n <ServiceException code="Service configuration error">Service unknown or unsupported</ServiceException>\n</ServiceExceptionReport>\n') self.assertEqual(self.strip_version_xmlns(body), expected) def test_pluginfilters(self): """Test python plugins filters""" try: from qgis.server import QgsServerFilter except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return class SimpleHelloFilter(QgsServerFilter): def requestReady(self): QgsMessageLog.logMessage("SimpleHelloFilter.requestReady") def sendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.sendResponse") def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() QgsMessageLog.logMessage("SimpleHelloFilter.responseComplete") if params.get('SERVICE', '').upper() == 'SIMPLE': request.clearHeaders() request.setHeader('Content-type', 'text/plain') request.clearBody() request.appendBody('Hello from SimpleServer!'.encode('utf-8')) serverIface = self.server.serverInterface() filter = SimpleHelloFilter(serverIface) serverIface.registerFilter(filter, 100) # Get registered filters self.assertEqual(filter, serverIface.filters()[100][0]) # Register some more filters class Filter1(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter1!'.encode('utf-8')) class Filter2(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter2!'.encode('utf-8')) filter1 = Filter1(serverIface) filter2 = Filter2(serverIface) serverIface.registerFilter(filter1, 101) serverIface.registerFilter(filter2, 200) serverIface.registerFilter(filter2, 100) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [_v for _v in self.server.handleRequest('service=simple')] response = header + body expected = b'Content-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Test that the bindings for complex type QgsServerFiltersMap are working filters = {100: [filter, filter2], 101: [filter1], 200: [filter2]} serverIface.setFilters(filters) self.assertTrue(filter in serverIface.filters()[100]) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [_v for _v in self.server.handleRequest('service=simple')] response = header + body expected = b'Content-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # WMS tests def wms_request_compare(self, request, extra=None, reference_file=None): project = self.testdata_path + "test_project.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3&REQUEST=%s' % (urllib.parse.quote(project), request) if extra is not None: query_string += extra header, body = self.server.handleRequest(query_string) response = header + body reference_path = self.testdata_path + (request.lower() if not reference_file else reference_file) + '.txt' f = open(reference_path, 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(reference_path, 'wb+') f.write(response) f.close() f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() #""" response = re.sub(RE_STRIP_UNCHECKABLE, b'*****', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'*****', expected) # for older GDAL versions (<2.0), id field will be integer type if int(osgeo.gdal.VersionInfo()[:1]) < 2: expected = expected.replace(b'typeName="Integer64" precision="0" length="10" editType="TextEdit" type="qlonglong"', b'typeName="Integer" precision="0" length="10" editType="TextEdit" type="int"') self.assertXMLEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected.decode('utf-8'), response.decode('utf-8'))) def test_project_wms(self): """Test some WMS request""" for request in ('GetCapabilities', 'GetProjectSettings'): self.wms_request_compare(request) # Test getfeatureinfo response self.wms_request_compare('GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + 'info_format=text%2Fhtml&transparent=true&' + 'width=600&height=400&srs=EPSG%3A3857&bbox=913190.6389747962%2C' + '5606005.488876367%2C913235.426296057%2C5606035.347090538&' + 'query_layers=testlayer%20%C3%A8%C3%A9&X=190&Y=320', 'wms_getfeatureinfo-text-html') # Test getfeatureinfo default info_format self.wms_request_compare('GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + 'transparent=true&' + 'width=600&height=400&srs=EPSG%3A3857&bbox=913190.6389747962%2C' + '5606005.488876367%2C913235.426296057%2C5606035.347090538&' + 'query_layers=testlayer%20%C3%A8%C3%A9&X=190&Y=320', 'wms_getfeatureinfo-text-plain') # Regression for #8656 # Mind the gap! (the space in the FILTER expression) self.wms_request_compare('GetFeatureInfo', '&layers=testlayer%20%C3%A8%C3%A9&' + 'INFO_FORMAT=text%2Fxml&' + 'width=600&height=400&srs=EPSG%3A3857&' + 'query_layers=testlayer%20%C3%A8%C3%A9&' + 'FEATURE_COUNT=10&FILTER=testlayer%20%C3%A8%C3%A9' + urllib.parse.quote(':"NAME" = \'two\''), 'wms_getfeatureinfo_filter') def wms_inspire_request_compare(self, request): """WMS INSPIRE tests""" project = self.testdata_path + "test_project_inspire.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WMS&VERSION=1.3.0&REQUEST=%s' % (urllib.parse.quote(project), request) header, body = self.server.handleRequest(query_string) response = header + body f = open(self.testdata_path + request.lower() + '_inspire.txt', 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_UNCHECKABLE, b'', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'', expected) self.assertXMLEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected.decode('utf-8'), response.decode('utf-8'))) def test_project_wms_inspire(self): """Test some WMS request""" for request in ('GetCapabilities',): self.wms_inspire_request_compare(request) # WFS tests def wfs_request_compare(self, request): project = self.testdata_path + "test_project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % (urllib.parse.quote(project), request) header, body = self.server.handleRequest(query_string) self.assert_headers(header, body) response = header + body f = open(self.testdata_path + 'wfs_' + request.lower() + '.txt', 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_' + request.lower() + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_UNCHECKABLE, b'', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'', expected) # for older GDAL versions (<2.0), id field will be integer type if int(osgeo.gdal.VersionInfo()[:1]) < 2: expected = expected.replace(b'<element type="long" name="id"/>', b'<element type="integer" name="id"/>') self.assertXMLEqual(response, expected, msg="request %s failed.\n Query: %s\n Expected:\n%s\n\n Response:\n%s" % (query_string, request, expected.decode('utf-8'), response.decode('utf-8'))) def test_project_wfs(self): """Test some WFS request""" for request in ('GetCapabilities', 'DescribeFeatureType'): self.wfs_request_compare(request) def wfs_getfeature_compare(self, requestid, request): project = self.testdata_path + "test_project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % (urllib.parse.quote(project), request) header, body = self.server.handleRequest(query_string) self.result_compare( 'wfs_getfeature_' + requestid + '.txt', "request %s failed.\n Query: %s" % ( query_string, request, ), header, body ) def result_compare(self, file_name, error_msg_header, header, body): self.assert_headers(header, body) response = header + body f = open(self.testdata_path + file_name, 'rb') expected = f.read() f.close() # Store the output for debug or to regenerate the reference documents: """ f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_expected.txt', 'w+') f.write(expected) f.close() f = open(os.path.dirname(__file__) + '/wfs_getfeature_' + requestid + '_response.txt', 'w+') f.write(response) f.close() """ response = re.sub(RE_STRIP_UNCHECKABLE, b'', response) expected = re.sub(RE_STRIP_UNCHECKABLE, b'', expected) self.assertXMLEqual(response, expected, msg="%s\n Expected:\n%s\n\n Response:\n%s" % (error_msg_header, str(expected, errors='replace'), str(response, errors='replace'))) def test_getfeature(self): tests = [] tests.append(('nobbox', 'GetFeature&TYPENAME=testlayer')) tests.append(('startindex2', 'GetFeature&TYPENAME=testlayer&STARTINDEX=2')) tests.append(('limit2', 'GetFeature&TYPENAME=testlayer&MAXFEATURES=2')) tests.append(('start1_limit1', 'GetFeature&TYPENAME=testlayer&MAXFEATURES=1&STARTINDEX=1')) for id, req in tests: self.wfs_getfeature_compare(id, req) def wfs_getfeature_post_compare(self, requestid, request): project = self.testdata_path + "test_project_wfs.qgs" assert os.path.exists(project), "Project file not found: " + project query_string = 'MAP={}'.format(urllib.parse.quote(project)) self.server.putenv("REQUEST_METHOD", "POST") self.server.putenv("REQUEST_BODY", request) header, body = self.server.handleRequest(query_string) self.server.putenv("REQUEST_METHOD", '') self.server.putenv("REQUEST_BODY", '') self.result_compare( 'wfs_getfeature_{}.txt'.format(requestid), "GetFeature in POST for '{}' failed.".format(requestid), header, body, ) def test_getfeature_post(self): template = """<?xml version="1.0" encoding="UTF-8"?> <wfs:GetFeature service="WFS" version="1.0.0" {} xmlns:wfs="http://www.opengis.net/wfs" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs http://schemas.opengis.net/wfs/1.1.0/wfs.xsd"> <wfs:Query typeName="testlayer" xmlns:feature="http://www.qgis.org/gml"> <ogc:Filter xmlns:ogc="http://www.opengis.net/ogc"> <ogc:BBOX> <ogc:PropertyName>geometry</ogc:PropertyName> <gml:Envelope xmlns:gml="http://www.opengis.net/gml"> <gml:lowerCorner>8 44</gml:lowerCorner> <gml:upperCorner>9 45</gml:upperCorner> </gml:Envelope> </ogc:BBOX> </ogc:Filter> </wfs:Query> </wfs:GetFeature> """ tests = [] tests.append(('nobbox', template.format(""))) tests.append(('startindex2', template.format('startIndex="2"'))) tests.append(('limit2', template.format('maxFeatures="2"'))) tests.append(('start1_limit1', template.format('startIndex="1" maxFeatures="1"'))) for id, req in tests: self.wfs_getfeature_post_compare(id, req) def test_getLegendGraphics(self): """Test that does not return an exception but an image""" parms = { 'MAP': self.testdata_path + "test_project.qgs", 'SERVICE': 'WMS', 'VERSION': '1.3.0', 'REQUEST': 'GetLegendGraphic', 'FORMAT': 'image/png', #'WIDTH': '20', # optional #'HEIGHT': '20', # optional 'LAYER': 'testlayer%20èé', } qs = '&'.join(["%s=%s" % (k, v) for k, v in parms.items()]) print(qs) h, r = self.server.handleRequest(qs) self.assertEqual(-1, h.find(b'Content-Type: text/xml; charset=utf-8'), "Header: %s\nResponse:\n%s" % (h, r)) self.assertNotEqual(-1, h.find(b'Content-Type: image/png'), "Header: %s\nResponse:\n%s" % (h, r)) def test_getLegendGraphics_layertitle(self): """Test that does not return an exception but an image""" parms = { 'MAP': self.testdata_path + "test_project.qgs", 'SERVICE': 'WMS', 'VERSION': '1.3.0', 'REQUEST': 'GetLegendGraphic', 'FORMAT': 'image/png', #'WIDTH': '20', # optional #'HEIGHT': '20', # optional 'LAYER': u'testlayer%20èé', 'LAYERTITLE': 'TRUE', } qs = '&'.join([u"%s=%s" % (k, v) for k, v in parms.items()]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_test", 250, QSize(10, 10)) parms = { 'MAP': self.testdata_path + "test_project.qgs", 'SERVICE': 'WMS', 'VERSION': '1.3.0', 'REQUEST': 'GetLegendGraphic', 'FORMAT': 'image/png', #'WIDTH': '20', # optional #'HEIGHT': '20', # optional 'LAYER': u'testlayer%20èé', 'LAYERTITLE': 'FALSE', } qs = '&'.join([u"%s=%s" % (k, v) for k, v in parms.items()]) r, h = self._result(self.server.handleRequest(qs)) self._img_diff_error(r, h, "WMS_GetLegendGraphic_test_layertitle_false", 250, QSize(10, 10)) def _result(self, data): headers = {} for line in data[0].decode('UTF-8').split("\n"): if line != "": header = line.split(":") self.assertEqual(len(header), 2, line) headers[str(header[0])] = str(header[1]).strip() return data[1], headers def _img_diff(self, image, control_image, max_diff, max_size_diff=QSize()): temp_image = os.path.join(tempfile.gettempdir(), "%s_result.png" % control_image) with open(temp_image, "wb") as f: f.write(image) control = QgsRenderChecker() control.setControlPathPrefix("qgis_server") control.setControlName(control_image) control.setRenderedImage(temp_image) if max_size_diff.isValid(): control.setSizeTolerance(max_size_diff.width(), max_size_diff.height()) return control.compareImages(control_image), control.report() def _img_diff_error(self, response, headers, image, max_diff=10, max_size_diff=QSize()): self.assertEqual( headers.get("Content-Type"), "image/png", "Content type is wrong: %s" % headers.get("Content-Type")) test, report = self._img_diff(response, image, max_diff, max_size_diff) with open(os.path.join(tempfile.gettempdir(), image + "_result.png"), "rb") as rendered_file: encoded_rendered_file = base64.b64encode(rendered_file.read()) message = "Image is wrong\n%s\nImage:\necho '%s' | base64 -d >%s/%s_result.png" % ( report, encoded_rendered_file.strip(), tempfile.gettempdir(), image ) with open(os.path.join(tempfile.gettempdir(), image + "_result_diff.png"), "rb") as diff_file: encoded_diff_file = base64.b64encode(diff_file.read()) message += "\nDiff:\necho '%s' | base64 -d > %s/%s_result_diff.png" % ( encoded_diff_file.strip(), tempfile.gettempdir(), image ) self.assertTrue(test, message)
class QgsServerTestBase(unittest.TestCase): """Base class for QGIS server tests""" # Set to True in child classes to re-generate reference files for this class regenerate_reference = False def assertXMLEqual(self, response, expected, msg=''): """Compare XML line by line and sorted attributes""" response_lines = response.splitlines() expected_lines = expected.splitlines() line_no = 1 self.assertEqual( len(expected_lines), len(response_lines), "Expected and response have different number of lines!\n{}".format( msg)) for expected_line in expected_lines: expected_line = expected_line.strip() response_line = response_lines[line_no - 1].strip() response_line = response_line.replace(b'e+6', b'e+06') # Compare tag if re.match(RE_ELEMENT, expected_line): expected_elements = re.findall(RE_ELEMENT, expected_line) response_elements = re.findall(RE_ELEMENT, response_line) self.assertEqual(expected_elements[0], response_elements[0], msg=msg + "\nTag mismatch on line %s: %s != %s" % (line_no, expected_line, response_line)) # Compare content if len(expected_elements ) == 2 and expected_elements[0] == expected_elements[1]: expected_element_content = re.findall( RE_ELEMENT_CONTENT, expected_line) response_element_content = re.findall( RE_ELEMENT_CONTENT, response_line) self.assertEqual( len(expected_element_content), len(response_element_content), msg=msg + "\nContent mismatch on line %s: %s != %s" % (line_no, expected_line, response_line)) if len(expected_element_content): self.assertEqual( expected_element_content[0], response_element_content[0], msg=msg + "\nContent mismatch on line %s: %s != %s" % (line_no, expected_line, response_line)) else: self.assertEqual(expected_line, response_line, msg=msg + "\nTag line mismatch %s: %s != %s\n%s" % (line_no, expected_line, response_line, msg)) # print("---->%s\t%s == %s" % (line_no, expected_line, response_line)) # Compare attributes if re.findall(RE_ATTRIBUTES, expected_line): # has attrs expected_attrs, expected_values = zip( *sorted(re.findall(RE_ATTRIBUTES, expected_line))) self.assertTrue( re.findall(RE_ATTRIBUTES, response_line), msg=msg + "\nXML attributes differ at line {0}: {1} != {2}".format( line_no, expected_line, response_line)) response_attrs, response_values = zip( *sorted(re.findall(RE_ATTRIBUTES, response_line))) self.assertEqual( expected_attrs, response_attrs, msg=msg + "\nXML attributes differ at line {0}: {1} != {2}".format( line_no, expected_attrs, response_attrs)) self.assertEqual( expected_values, response_values, msg=msg + "\nXML attribute values differ at line {0}: {1} != {2}". format(line_no, expected_values, response_values)) line_no += 1 @classmethod def setUpClass(cls): cls.app = QgsApplication([], False) @classmethod def tearDownClass(cls): cls.app.exitQgis() def setUp(self): """Create the server instance""" self.fontFamily = QgsFontUtils.standardTestFontFamily() QgsFontUtils.loadStandardTestFonts(['All']) self.testdata_path = unitTestDataPath('qgis_server') + '/' d = unitTestDataPath('qgis_server_accesscontrol') + '/' self.projectPath = os.path.join(d, "project.qgs") self.projectAnnotationPath = os.path.join( d, "project_with_annotations.qgs") self.projectStatePath = os.path.join(d, "project_state.qgs") self.projectUseLayerIdsPath = os.path.join(d, "project_use_layerids.qgs") self.projectGroupsPath = os.path.join(d, "project_groups.qgs") # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def strip_version_xmlns(self, text): """Order of attributes is random, strip version and xmlns""" return text.replace(b'version="1.3.0"', b'').replace(b'xmlns="http://www.opengis.net/ogc"', b'') def assert_headers(self, header, body): stream = StringIO() header_string = header.decode('utf-8') stream.write(header_string) headers = email.message_from_string(header_string) if 'content-length' in headers: content_length = int(headers['content-length']) body_length = len(body) self.assertEqual( content_length, body_length, msg= "Header reported content-length: %d Actual body length was: %d" % (content_length, body_length)) @classmethod def store_reference(self, reference_path, response): """Utility to store reference files""" # Normally this is false if not self.regenerate_reference: return # Store the output for debug or to regenerate the reference documents: f = open(reference_path, 'wb+') f.write(response) f.close() def _result(self, data): headers = {} for line in data[0].decode('UTF-8').split("\n"): if line != "": header = line.split(":") self.assertEqual(len(header), 2, line) headers[str(header[0])] = str(header[1]).strip() return data[1], headers def _img_diff(self, image, control_image, max_diff, max_size_diff=QSize()): temp_image = os.path.join(tempfile.gettempdir(), "%s_result.png" % control_image) with open(temp_image, "wb") as f: f.write(image) control = QgsRenderChecker() control.setControlPathPrefix("qgis_server") control.setControlName(control_image) control.setRenderedImage(temp_image) if max_size_diff.isValid(): control.setSizeTolerance(max_size_diff.width(), max_size_diff.height()) return control.compareImages(control_image, max_diff), control.report() def _img_diff_error(self, response, headers, image, max_diff=100, max_size_diff=QSize()): self.assertEqual( headers.get("Content-Type"), "image/png", "Content type is wrong: %s\n%s" % (headers.get("Content-Type"), response)) test, report = self._img_diff(response, image, max_diff, max_size_diff) with open(os.path.join(tempfile.gettempdir(), image + "_result.png"), "rb") as rendered_file: encoded_rendered_file = base64.b64encode(rendered_file.read()) if not os.environ.get('ENCODED_OUTPUT'): message = "Image is wrong\: rendered file %s/%s_result.png" % ( tempfile.gettempdir(), image) else: message = "Image is wrong\n%s\nImage:\necho '%s' | base64 -d >%s/%s_result.png" % ( report, encoded_rendered_file.strip().decode('utf8'), tempfile.gettempdir(), image) # If the failure is in image sizes the diff file will not exists. if os.path.exists( os.path.join(tempfile.gettempdir(), image + "_result_diff.png")): with open( os.path.join(tempfile.gettempdir(), image + "_result_diff.png"), "rb") as diff_file: if not os.environ.get('ENCODED_OUTPUT'): message = "Image is wrong\: diff file %s/%s_result_diff.png" % ( tempfile.gettempdir(), image) else: encoded_diff_file = base64.b64encode(diff_file.read()) message += "\nDiff:\necho '%s' | base64 -d > %s/%s_result_diff.png" % ( encoded_diff_file.strip().decode('utf8'), tempfile.gettempdir(), image) self.assertTrue(test, message) def _execute_request(self, qs, requestMethod=QgsServerRequest.GetMethod, data=None): request = QgsBufferServerRequest(qs, requestMethod, {}, data) response = QgsBufferServerResponse() self.server.handleRequest(request, response) headers = [] rh = response.headers() rk = sorted(rh.keys()) for k in rk: headers.append(("%s: %s" % (k, rh[k])).encode('utf-8')) return b"\n".join(headers) + b"\n\n", bytes(response.body()) def _execute_request_project(self, qs, project, requestMethod=QgsServerRequest.GetMethod, data=None): request = QgsBufferServerRequest(qs, requestMethod, {}, data) response = QgsBufferServerResponse() self.server.handleRequest(request, response, project) headers = [] rh = response.headers() rk = sorted(rh.keys()) for k in rk: headers.append(("%s: %s" % (k, rh[k])).encode('utf-8')) return b"\n".join(headers) + b"\n\n", bytes(response.body())
def setUp(self): self.server = QgsServer() copyfile(self.db, self.db_clone)
class TestQgsServerPlugins(QgsServerTestBase): def setUp(self): """Create the server instance""" self.testdata_path = unitTestDataPath('qgis_server') + '/' d = unitTestDataPath('qgis_server_accesscontrol') + '/' self.projectPath = os.path.join(d, "project.qgs") # Clean env just to be sure env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE'] for ev in env_vars: try: del os.environ[ev] except KeyError: pass self.server = QgsServer() def test_pluginfilters(self): """Test python plugins filters""" try: from qgis.server import QgsServerFilter except ImportError: print("QGIS Server plugins are not compiled. Skipping test") return class SimpleHelloFilter(QgsServerFilter): def requestReady(self): QgsMessageLog.logMessage("SimpleHelloFilter.requestReady") def sendResponse(self): QgsMessageLog.logMessage("SimpleHelloFilter.sendResponse") def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() QgsMessageLog.logMessage("SimpleHelloFilter.responseComplete") if params.get('SERVICE', '').upper() == 'SIMPLE': request.clear() request.setResponseHeader('Content-type', 'text/plain') request.appendBody( 'Hello from SimpleServer!'.encode('utf-8')) serverIface = self.server.serverInterface() filter = SimpleHelloFilter(serverIface) serverIface.registerFilter(filter, 100) # Get registered filters self.assertEqual(filter, serverIface.filters()[100][0]) # global to be modified inside plugin filters globals()['status_code'] = 0 # body to be checked inside plugin filters globals()['body2'] = None # headers to be checked inside plugin filters globals()['headers2'] = None # Register some more filters class Filter1(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter1!'.encode('utf-8')) class Filter2(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'SIMPLE': request.appendBody('Hello from Filter2!'.encode('utf-8')) class Filter3(QgsServerFilter): """Test get and set status code""" def responseComplete(self): global status_code request = self.serverInterface().requestHandler() request.setStatusCode(999) status_code = request.statusCode() class Filter4(QgsServerFilter): """Body getter""" def responseComplete(self): global body2 request = self.serverInterface().requestHandler() body2 = request.body() class Filter5(QgsServerFilter): """Body setter, clear body, keep headers""" def responseComplete(self): global headers2 request = self.serverInterface().requestHandler() request.clearBody() headers2 = request.responseHeaders() request.appendBody('new body, new life!'.encode('utf-8')) filter1 = Filter1(serverIface) filter2 = Filter2(serverIface) filter3 = Filter3(serverIface) filter4 = Filter4(serverIface) serverIface.registerFilter(filter1, 101) serverIface.registerFilter(filter2, 200) serverIface.registerFilter(filter2, 100) serverIface.registerFilter(filter3, 300) serverIface.registerFilter(filter4, 400) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [_v for _v in self._execute_request('?service=simple')] response = header + body expected = b'Content-Length: 62\nContent-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Check status code self.assertEqual(status_code, 999) # Check body getter from filter self.assertEqual( body2, b'Hello from SimpleServer!Hello from Filter1!Hello from Filter2!') # Check that the bindings for complex type QgsServerFiltersMap are working filters = {100: [filter, filter2], 101: [filter1], 200: [filter2]} serverIface.setFilters(filters) self.assertTrue(filter in serverIface.filters()[100]) self.assertTrue(filter2 in serverIface.filters()[100]) self.assertEqual(filter1, serverIface.filters()[101][0]) self.assertEqual(filter2, serverIface.filters()[200][0]) header, body = [_v for _v in self._execute_request('?service=simple')] response = header + body expected = b'Content-Length: 62\nContent-type: text/plain\n\nHello from SimpleServer!Hello from Filter1!Hello from Filter2!' self.assertEqual(response, expected) # Now, re-run with body setter filter5 = Filter5(serverIface) serverIface.registerFilter(filter5, 500) header, body = [_v for _v in self._execute_request('?service=simple')] response = header + body expected = b'Content-Length: 19\nContent-type: text/plain\n\nnew body, new life!' self.assertEqual(response, expected) self.assertEqual(headers2, {'Content-type': 'text/plain'})
QGIS_SERVER_PKI_KEY = os.environ.get('QGIS_SERVER_PKI_KEY') QGIS_SERVER_PKI_AUTHORITY = os.environ.get('QGIS_SERVER_PKI_AUTHORITY') QGIS_SERVER_PKI_USERNAME = os.environ.get('QGIS_SERVER_PKI_USERNAME') # Check if PKI - https is enabled https = (QGIS_SERVER_PKI_CERTIFICATE is not None and os.path.isfile(QGIS_SERVER_PKI_CERTIFICATE) and QGIS_SERVER_PKI_KEY is not None and os.path.isfile(QGIS_SERVER_PKI_KEY) and QGIS_SERVER_PKI_AUTHORITY is not None and os.path.isfile(QGIS_SERVER_PKI_AUTHORITY) and QGIS_SERVER_PKI_USERNAME) qgs_app = QgsApplication([], False) qgs_server = QgsServer() if os.environ.get('QGIS_SERVER_HTTP_BASIC_AUTH') is not None: from qgis.server import QgsServerFilter import base64 class HTTPBasicFilter(QgsServerFilter): def responseComplete(self): request = self.serverInterface().requestHandler() if self.serverInterface().getEnv('HTTP_AUTHORIZATION'): username, password = base64.b64decode(self.serverInterface().getEnv('HTTP_AUTHORIZATION')[6:]).split(b':') if (username.decode('utf-8') == os.environ.get('QGIS_SERVER_USERNAME', 'username') and password.decode('utf-8') == os.environ.get('QGIS_SERVER_PASSWORD', 'password')): return
QGIS_SERVER_PKI_CERTIFICATE = os.environ.get('QGIS_SERVER_PKI_CERTIFICATE') QGIS_SERVER_PKI_KEY = os.environ.get('QGIS_SERVER_PKI_KEY') QGIS_SERVER_PKI_AUTHORITY = os.environ.get('QGIS_SERVER_PKI_AUTHORITY') QGIS_SERVER_PKI_USERNAME = os.environ.get('QGIS_SERVER_PKI_USERNAME') # Check if PKI - https is enabled https = (QGIS_SERVER_PKI_CERTIFICATE is not None and os.path.isfile(QGIS_SERVER_PKI_CERTIFICATE) and QGIS_SERVER_PKI_KEY is not None and os.path.isfile(QGIS_SERVER_PKI_KEY) and QGIS_SERVER_PKI_AUTHORITY is not None and os.path.isfile(QGIS_SERVER_PKI_AUTHORITY) and QGIS_SERVER_PKI_USERNAME) qgs_app = QgsApplication([], False) qgs_server = QgsServer() # Load plugins if os.environ.get('QGIS_PLUGINPATH', False): plugindir = os.environ.get('QGIS_PLUGINPATH') sys.path.append(plugindir) # Magic mistery: without this, virtual QgsServerFilter instances are not wrapped iface = qgs_server.serverInterface() try: for modulename in [(a, b, c) for a, b, c in os.walk(plugindir)][0][1]: try: module = importlib.import_module(modulename) getattr(module, 'serverClassFactory')(iface) QgsMessageLog.logMessage('Python plugin %s loaded!' % modulename) except: