1
+ #!/usr/bin/env python3
1
2
# -*- coding: utf-8 -*-
2
3
"""
3
- QGIS Server HTTP wrapper
4
+ QGIS Server HTTP wrapper for testing purposes
5
+ ================================================================================
4
6
5
7
This script launches a QGIS Server listening on port 8081 or on the port
6
8
specified on the environment variable QGIS_SERVER_PORT.
7
- QGIS_SERVER_HOST (defaults to 127.0.0.1)
9
+ Hostname is set by environment variable QGIS_SERVER_HOST (defaults to 127.0.0.1)
10
+
11
+ The server can be configured to support any of the following auth systems
12
+ (mutually exclusive):
13
+
14
+ * PKI
15
+ * HTTP Basic
16
+ * OAuth2 (requires python package oauthlib, installable with:
17
+ with "pip install oauthlib")
18
+
19
+
20
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
21
+ SECURITY WARNING:
22
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
23
+
24
+ This script was developed for testing purposes and was not meant to be secure,
25
+ please do not use in a production server any of the authentication systems
26
+ implemented here.
27
+
28
+
29
+ HTTPS
30
+ --------------------------------------------------------------------------------
31
+
32
+ HTTPS is automatically enabled for PKI and OAuth2
33
+
34
+
35
+ HTTP Basic
36
+ --------------------------------------------------------------------------------
8
37
9
38
For testing purposes, HTTP Basic can be enabled by setting the following
10
39
environment variables:
13
42
* QGIS_SERVER_USERNAME (default ="username")
14
43
* QGIS_SERVER_PASSWORD (default ="password")
15
44
45
+
46
+ PKI
47
+ --------------------------------------------------------------------------------
48
+
16
49
PKI authentication with HTTPS can be enabled with:
17
50
18
51
* QGIS_SERVER_PKI_CERTIFICATE (server certificate)
19
52
* QGIS_SERVER_PKI_KEY (server private key)
20
53
* QGIS_SERVER_PKI_AUTHORITY (root CA)
21
54
* QGIS_SERVER_PKI_USERNAME (valid username)
22
55
23
- Sample run:
56
+
57
+ OAuth2 Resource Owner Grant Flow
58
+ --------------------------------------------------------------------------------
59
+
60
+ OAuth2 Resource Owner Grant Flow with HTTPS can be enabled with:
61
+
62
+ * QGIS_SERVER_OAUTH2_AUTHORITY (no default)
63
+ * QGIS_SERVER_OAUTH2_KEY (server private key)
64
+ * QGIS_SERVER_OAUTH2_CERTIFICATE (server certificate)
65
+ * QGIS_SERVER_OAUTH2_USERNAME (default ="username")
66
+ * QGIS_SERVER_OAUTH2_PASSWORD (default ="password")
67
+ * QGIS_SERVER_OAUTH2_TOKEN_EXPIRES_IN (default = 3600)
68
+
69
+ Available endpoints:
70
+
71
+ - /token (returns a new access_token),
72
+ optionally specify an expiration time in seconds with ?ttl=<int>
73
+ - /refresh (returns a new access_token from a refresh token),
74
+ optionally specify an expiration time in seconds with ?ttl=<int>
75
+ - /result (check the Bearer token and returns a short sentence if it validates)
76
+
77
+
78
+ Sample runs
79
+ --------------------------------------------------------------------------------
80
+
81
+ PKI:
24
82
25
83
QGIS_SERVER_PKI_USERNAME=Gerardus QGIS_SERVER_PORT=47547 QGIS_SERVER_HOST=localhost \
26
84
QGIS_SERVER_PKI_KEY=/home/$USER/dev/QGIS/tests/testdata/auth_system/certs_keys/localhost_ssl_key.pem \
27
85
QGIS_SERVER_PKI_CERTIFICATE=/home/$USER/dev/QGIS/tests/testdata/auth_system/certs_keys/localhost_ssl_cert.pem \
28
86
QGIS_SERVER_PKI_AUTHORITY=/home/$USER/dev/QGIS/tests/testdata/auth_system/certs_keys/chains_subissuer-issuer-root_issuer2-root2.pem \
29
87
python3 /home/$USER/dev/QGIS/tests/src/python/qgis_wrapped_server.py
30
88
89
+
90
+ OAuth2:
91
+
92
+ QGIS_SERVER_PORT=8443 \
93
+ QGIS_SERVER_HOST=localhost \
94
+ QGIS_SERVER_OAUTH2_AUTHORITY=/home/$USER/dev/QGIS/tests/testdata/auth_system/certs_keys/chain_subissuer-issuer-root.pem \
95
+ QGIS_SERVER_OAUTH2_CERTIFICATE=/home/$USER/dev/QGIS/tests/testdata/auth_system/certs_keys/localhost_ssl_cert.pem \
96
+ QGIS_SERVER_OAUTH2_KEY=/home/$USER/dev/QGIS/tests/testdata/auth_system/certs_keys/localhost_ssl_key.pem \
97
+ python3 /home/$USER/dev/QGIS/tests/src/python/qgis_wrapped_server.py
98
+
99
+
31
100
.. note:: This program is free software; you can redistribute it and/or modify
32
101
it under the terms of the GNU General Public License as published by
33
102
the Free Software Foundation; either version 2 of the License, or
49
118
import sys
50
119
import signal
51
120
import ssl
121
+ import copy
52
122
import urllib .parse
53
123
from http .server import BaseHTTPRequestHandler , HTTPServer
54
124
from qgis .core import QgsApplication
55
125
from qgis .server import QgsServer , QgsServerRequest , QgsBufferServerRequest , QgsBufferServerResponse
56
126
57
127
QGIS_SERVER_PORT = int (os .environ .get ('QGIS_SERVER_PORT' , '8081' ))
58
128
QGIS_SERVER_HOST = os .environ .get ('QGIS_SERVER_HOST' , '127.0.0.1' )
129
+
130
+ # HTTP Basic
131
+ QGIS_SERVER_HTTP_BASIC_AUTH = os .environ .get ('QGIS_SERVER_HTTP_BASIC_AUTH' , False )
132
+ QGIS_SERVER_USERNAME = os .environ .get ('QGIS_SERVER_USERNAME' , 'username' )
133
+ QGIS_SERVER_PASSWORD = os .environ .get ('QGIS_SERVER_PASSWORD' , 'password' )
134
+
59
135
# PKI authentication
60
136
QGIS_SERVER_PKI_CERTIFICATE = os .environ .get ('QGIS_SERVER_PKI_CERTIFICATE' )
61
137
QGIS_SERVER_PKI_KEY = os .environ .get ('QGIS_SERVER_PKI_KEY' )
62
138
QGIS_SERVER_PKI_AUTHORITY = os .environ .get ('QGIS_SERVER_PKI_AUTHORITY' )
63
139
QGIS_SERVER_PKI_USERNAME = os .environ .get ('QGIS_SERVER_PKI_USERNAME' )
64
140
65
- # Check if PKI - https is enabled
66
- https = (QGIS_SERVER_PKI_CERTIFICATE is not None and
67
- os .path .isfile (QGIS_SERVER_PKI_CERTIFICATE ) and
68
- QGIS_SERVER_PKI_KEY is not None and
69
- os .path .isfile (QGIS_SERVER_PKI_KEY ) and
70
- QGIS_SERVER_PKI_AUTHORITY is not None and
71
- os .path .isfile (QGIS_SERVER_PKI_AUTHORITY ) and
72
- QGIS_SERVER_PKI_USERNAME )
141
+ # OAuth2 authentication
142
+ QGIS_SERVER_OAUTH2_CERTIFICATE = os .environ .get ('QGIS_SERVER_OAUTH2_CERTIFICATE' )
143
+ QGIS_SERVER_OAUTH2_KEY = os .environ .get ('QGIS_SERVER_OAUTH2_KEY' )
144
+ QGIS_SERVER_OAUTH2_AUTHORITY = os .environ .get ('QGIS_SERVER_OAUTH2_AUTHORITY' )
145
+ QGIS_SERVER_OAUTH2_USERNAME = os .environ .get ('QGIS_SERVER_OAUTH2_USERNAME' , 'username' )
146
+ QGIS_SERVER_OAUTH2_PASSWORD = os .environ .get ('QGIS_SERVER_OAUTH2_PASSWORD' , 'password' )
147
+ QGIS_SERVER_OAUTH2_TOKEN_EXPIRES_IN = os .environ .get ('QGIS_SERVER_OAUTH2_TOKEN_EXPIRES_IN' , 3600 )
148
+
149
+ # Check if PKI is enabled
150
+ QGIS_SERVER_PKI_AUTH = (
151
+ QGIS_SERVER_PKI_CERTIFICATE is not None and
152
+ os .path .isfile (QGIS_SERVER_PKI_CERTIFICATE ) and
153
+ QGIS_SERVER_PKI_KEY is not None and
154
+ os .path .isfile (QGIS_SERVER_PKI_KEY ) and
155
+ QGIS_SERVER_PKI_AUTHORITY is not None and
156
+ os .path .isfile (QGIS_SERVER_PKI_AUTHORITY ) and
157
+ QGIS_SERVER_PKI_USERNAME )
158
+
159
+ # Check if OAuth2 is enabled
160
+ QGIS_SERVER_OAUTH2_AUTH = (
161
+ QGIS_SERVER_OAUTH2_CERTIFICATE is not None and
162
+ os .path .isfile (QGIS_SERVER_OAUTH2_CERTIFICATE ) and
163
+ QGIS_SERVER_OAUTH2_KEY is not None and
164
+ os .path .isfile (QGIS_SERVER_OAUTH2_KEY ) and
165
+ QGIS_SERVER_OAUTH2_AUTHORITY is not None and
166
+ os .path .isfile (QGIS_SERVER_OAUTH2_AUTHORITY ) and
167
+ QGIS_SERVER_OAUTH2_USERNAME and QGIS_SERVER_OAUTH2_PASSWORD )
168
+
169
+ HTTPS_ENABLED = QGIS_SERVER_PKI_AUTH or QGIS_SERVER_OAUTH2_AUTH
73
170
74
171
75
172
qgs_app = QgsApplication ([], False )
76
173
qgs_server = QgsServer ()
77
174
78
175
79
- if os . environ . get ( ' QGIS_SERVER_HTTP_BASIC_AUTH' ) is not None :
176
+ if QGIS_SERVER_HTTP_BASIC_AUTH :
80
177
from qgis .server import QgsServerFilter
81
178
import base64
82
179
83
180
class HTTPBasicFilter (QgsServerFilter ):
84
181
85
182
def responseComplete (self ):
86
183
handler = self .serverInterface ().requestHandler ()
87
- auth = self . serverInterface (). requestHandler () .requestHeader ('HTTP_AUTHORIZATION' )
184
+ auth = handler .requestHeader ('HTTP_AUTHORIZATION' )
88
185
if auth :
89
186
username , password = base64 .b64decode (auth [6 :]).split (b':' )
90
187
if (username .decode ('utf-8' ) == os .environ .get ('QGIS_SERVER_USERNAME' , 'username' ) and
@@ -100,6 +197,139 @@ def responseComplete(self):
100
197
qgs_server .serverInterface ().registerFilter (filter )
101
198
102
199
200
+ if QGIS_SERVER_OAUTH2_AUTH :
201
+ from qgis .server import QgsServerFilter
202
+ from oauthlib .oauth2 import RequestValidator , LegacyApplicationServer
203
+ import base64
204
+ from datetime import datetime
205
+
206
+ # Naive token storage implementation
207
+ _tokens = {}
208
+
209
+ class SimpleValidator (RequestValidator ):
210
+ """Validate username and password
211
+ Note: does not support scopes or client_id"""
212
+
213
+ def validate_client_id (self , client_id , request ):
214
+ return True
215
+
216
+ def authenticate_client (self , request , * args , ** kwargs ):
217
+ """Wide open"""
218
+ request .client = type ("Client" , (), {'client_id' : 'my_id' })
219
+ return True
220
+
221
+ def validate_user (self , username , password , client , request , * args , ** kwargs ):
222
+ if username == QGIS_SERVER_OAUTH2_USERNAME and password == QGIS_SERVER_OAUTH2_PASSWORD :
223
+ return True
224
+ return False
225
+
226
+ def validate_grant_type (self , client_id , grant_type , client , request , * args , ** kwargs ):
227
+ # Clients should only be allowed to use one type of grant.
228
+ return grant_type in ('password' , 'refresh_token' )
229
+
230
+ def get_default_scopes (self , client_id , request , * args , ** kwargs ):
231
+ # Scopes a client will authorize for if none are supplied in the
232
+ # authorization request.
233
+ return ('my_scope' , )
234
+
235
+ def validate_scopes (self , client_id , scopes , client , request , * args , ** kwargs ):
236
+ """Wide open"""
237
+ return True
238
+
239
+ def save_bearer_token (self , token , request , * args , ** kwargs ):
240
+ # Remember to associate it with request.scopes, request.user and
241
+ # request.client. The two former will be set when you validate
242
+ # the authorization code. Don't forget to save both the
243
+ # access_token and the refresh_token and set expiration for the
244
+ # access_token to now + expires_in seconds.
245
+ _tokens [token ['access_token' ]] = copy .copy (token )
246
+ _tokens [token ['access_token' ]]['expiration' ] = datetime .now ().timestamp () + int (token ['expires_in' ])
247
+
248
+ def validate_bearer_token (self , token , scopes , request ):
249
+ """Check the token"""
250
+ return token in _tokens and _tokens [token ]['expiration' ] > datetime .now ().timestamp ()
251
+
252
+ def validate_refresh_token (self , refresh_token , client , request , * args , ** kwargs ):
253
+ """Ensure the Bearer token is valid and authorized access to scopes."""
254
+ for t in _tokens .values ():
255
+ if t ['refresh_token' ] == refresh_token :
256
+ return True
257
+ return False
258
+
259
+ def get_original_scopes (self , refresh_token , request , * args , ** kwargs ):
260
+ """Get the list of scopes associated with the refresh token."""
261
+ return []
262
+
263
+ validator = SimpleValidator ()
264
+ oauth_server = LegacyApplicationServer (validator , token_expires_in = QGIS_SERVER_OAUTH2_TOKEN_EXPIRES_IN )
265
+
266
+ class OAuth2Filter (QgsServerFilter ):
267
+ """This filter provides testing endpoint for OAuth2 Resource Owner Grant Flow
268
+
269
+ Available endpoints:
270
+ - /token (returns a new access_token),
271
+ optionally specify an expiration time in seconds with ?ttl=<int>
272
+ - /refresh (returns a new access_token from a refresh token),
273
+ optionally specify an expiration time in seconds with ?ttl=<int>
274
+ - /result (check the Bearer token and returns a short sentence if it validates)
275
+ """
276
+
277
+ def responseComplete (self ):
278
+
279
+ handler = self .serverInterface ().requestHandler ()
280
+
281
+ def _token (ttl ):
282
+ """Common code for new and refresh token"""
283
+ handler .clear ()
284
+ body = bytes (handler .data ()).decode ('utf8' )
285
+ old_expires_in = oauth_server .default_token_type .expires_in
286
+ # Hacky way to dynamically set token expiration time
287
+ oauth_server .default_token_type .expires_in = ttl
288
+ headers , payload , code = oauth_server .create_token_response ('/token' , 'post' , body , {})
289
+ oauth_server .default_token_type .expires_in = old_expires_in
290
+ for k , v in headers .items ():
291
+ handler .setResponseHeader (k , v )
292
+ handler .setStatusCode (code )
293
+ handler .appendBody (payload .encode ('utf-8' ))
294
+
295
+ # Token expiration
296
+ ttl = handler .parameterMap ().get ('TTL' , QGIS_SERVER_OAUTH2_TOKEN_EXPIRES_IN )
297
+ # Issue a new token
298
+ if handler .url ().find ('/token' ) != - 1 :
299
+ _token (ttl )
300
+ return
301
+
302
+ # Refresh token
303
+ if handler .url ().find ('/refresh' ) != - 1 :
304
+ _token (ttl )
305
+ return
306
+
307
+ # Check for valid token
308
+ auth = handler .requestHeader ('HTTP_AUTHORIZATION' )
309
+ if auth :
310
+ result , response = oauth_server .verify_request (handler .url (), 'post' , '' , {'Authorization' : auth })
311
+ if result :
312
+ # This is a test endpoint for OAuth2, it requires a valid token
313
+ if handler .url ().find ('/result' ) != - 1 :
314
+ handler .clear ()
315
+ handler .appendBody (b'Valid Token: enjoy OAuth2' )
316
+ # Standard flow
317
+ return
318
+ else :
319
+ # Wrong token, default response 401
320
+ pass
321
+
322
+ # No auth ...
323
+ handler .clear ()
324
+ handler .setStatusCode (401 )
325
+ handler .setResponseHeader ('Status' , '401 Unauthorized' )
326
+ handler .setResponseHeader ('WWW-Authenticate' , 'Bearer realm="QGIS Server"' )
327
+ handler .appendBody (b'Invalid Token: Authorization required.' )
328
+
329
+ filter = OAuth2Filter (qgs_server .serverInterface ())
330
+ qgs_server .serverInterface ().registerFilter (filter )
331
+
332
+
103
333
class Handler (BaseHTTPRequestHandler ):
104
334
105
335
def do_GET (self , post_body = None ):
@@ -130,16 +360,29 @@ def do_POST(self):
130
360
131
361
if __name__ == '__main__' :
132
362
server = HTTPServer ((QGIS_SERVER_HOST , QGIS_SERVER_PORT ), Handler )
133
- if https :
134
- server .socket = ssl .wrap_socket (server .socket ,
135
- certfile = QGIS_SERVER_PKI_CERTIFICATE ,
136
- keyfile = QGIS_SERVER_PKI_KEY ,
137
- ca_certs = QGIS_SERVER_PKI_AUTHORITY ,
138
- cert_reqs = ssl .CERT_REQUIRED ,
139
- server_side = True ,
140
- ssl_version = ssl .PROTOCOL_TLSv1 )
363
+ # HTTPS is enabled if any of PKI or OAuth2 are enabled too
364
+ if HTTPS_ENABLED :
365
+ if QGIS_SERVER_OAUTH2_AUTH :
366
+ server .socket = ssl .wrap_socket (
367
+ server .socket ,
368
+ certfile = QGIS_SERVER_OAUTH2_CERTIFICATE ,
369
+ ca_certs = QGIS_SERVER_OAUTH2_AUTHORITY ,
370
+ keyfile = QGIS_SERVER_OAUTH2_KEY ,
371
+ server_side = True ,
372
+ #cert_reqs=ssl.CERT_REQUIRED, # No certs for OAuth2
373
+ ssl_version = ssl .PROTOCOL_TLSv1 )
374
+ else :
375
+ server .socket = ssl .wrap_socket (
376
+ server .socket ,
377
+ certfile = QGIS_SERVER_PKI_CERTIFICATE ,
378
+ keyfile = QGIS_SERVER_PKI_KEY ,
379
+ ca_certs = QGIS_SERVER_PKI_AUTHORITY ,
380
+ cert_reqs = ssl .CERT_REQUIRED ,
381
+ server_side = True ,
382
+ ssl_version = ssl .PROTOCOL_TLSv1 )
383
+
141
384
print ('Starting server on %s://%s:%s, use <Ctrl-C> to stop' %
142
- ('https' if https else 'http' , QGIS_SERVER_HOST , server .server_port ), flush = True )
385
+ ('https' if HTTPS_ENABLED else 'http' , QGIS_SERVER_HOST , server .server_port ), flush = True )
143
386
144
387
def signal_handler (signal , frame ):
145
388
global qgs_app
0 commit comments