15
15
try : # Python 3
16
16
from http .server import HTTPServer , BaseHTTPRequestHandler
17
17
from urllib .parse import urlparse , parse_qs , urlencode
18
+ from html import escape
18
19
except ImportError : # Fall back to Python 2
19
20
from BaseHTTPServer import HTTPServer , BaseHTTPRequestHandler
20
21
from urlparse import urlparse , parse_qs
21
22
from urllib import urlencode
23
+ from cgi import escape
22
24
23
25
24
26
logger = logging .getLogger (__name__ )
@@ -77,25 +79,42 @@ def _qs2kv(qs):
77
79
for k , v in qs .items ()}
78
80
79
81
82
+ def _is_html (text ):
83
+ return text .startswith ("<" ) # Good enough for our purpose
84
+
85
+
86
+ def _escape (key_value_pairs ):
87
+ return {k : escape (v ) for k , v in key_value_pairs .items ()}
88
+
89
+
80
90
class _AuthCodeHandler (BaseHTTPRequestHandler ):
81
91
def do_GET (self ):
82
92
# For flexibility, we choose to not check self.path matching redirect_uri
83
93
#assert self.path.startswith('/THE_PATH_REGISTERED_BY_THE_APP')
84
94
qs = parse_qs (urlparse (self .path ).query )
85
95
if qs .get ('code' ) or qs .get ("error" ): # So, it is an auth response
86
- self .server .auth_response = _qs2kv (qs )
87
- logger .debug ("Got auth response: %s" , self .server .auth_response )
88
- template = (self .server .success_template
89
- if "code" in qs else self .server .error_template )
90
- self ._send_full_response (
91
- template .safe_substitute (** self .server .auth_response ))
92
- # NOTE: Don't do self.server.shutdown() here. It'll halt the server.
96
+ auth_response = _qs2kv (qs )
97
+ logger .debug ("Got auth response: %s" , auth_response )
98
+ if self .server .auth_state and self .server .auth_state != auth_response .get ("state" ):
99
+ # OAuth2 successful and error responses contain state when it was used
100
+ # https://www.rfc-editor.org/rfc/rfc6749#section-4.2.2.1
101
+ self ._send_full_response ("State mismatch" ) # Possibly an attack
102
+ else :
103
+ template = (self .server .success_template
104
+ if "code" in qs else self .server .error_template )
105
+ if _is_html (template .template ):
106
+ safe_data = _escape (auth_response ) # Foiling an XSS attack
107
+ else :
108
+ safe_data = auth_response
109
+ self ._send_full_response (template .safe_substitute (** safe_data ))
110
+ self .server .auth_response = auth_response # Set it now, after the response is likely sent
93
111
else :
94
112
self ._send_full_response (self .server .welcome_page )
113
+ # NOTE: Don't do self.server.shutdown() here. It'll halt the server.
95
114
96
115
def _send_full_response (self , body , is_ok = True ):
97
116
self .send_response (200 if is_ok else 400 )
98
- content_type = 'text/html' if body . startswith ( '<' ) else 'text/plain'
117
+ content_type = 'text/html' if _is_html ( body ) else 'text/plain'
99
118
self .send_header ('Content-type' , content_type )
100
119
self .end_headers ()
101
120
self .wfile .write (body .encode ("utf-8" ))
@@ -281,16 +300,14 @@ def _get_auth_response(self, result, auth_uri=None, timeout=None, state=None,
281
300
282
301
self ._server .timeout = timeout # Otherwise its handle_timeout() won't work
283
302
self ._server .auth_response = {} # Shared with _AuthCodeHandler
303
+ self ._server .auth_state = state # So handler will check it before sending response
284
304
while not self ._closing : # Otherwise, the handle_request() attempt
285
305
# would yield noisy ValueError trace
286
306
# Derived from
287
307
# https://docs.python.org/2/library/basehttpserver.html#more-examples
288
308
self ._server .handle_request ()
289
309
if self ._server .auth_response :
290
- if state and state != self ._server .auth_response .get ("state" ):
291
- logger .debug ("State mismatch. Ignoring this noise." )
292
- else :
293
- break
310
+ break
294
311
result .update (self ._server .auth_response ) # Return via writable result param
295
312
296
313
def close (self ):
@@ -318,6 +335,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
318
335
default = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" )
319
336
p .add_argument ('client_id' , help = "The client_id of your application" )
320
337
p .add_argument ('--port' , type = int , default = 0 , help = "The port in redirect_uri" )
338
+ p .add_argument ('--timeout' , type = int , default = 60 , help = "Timeout value, in second" )
321
339
p .add_argument ('--host' , default = "127.0.0.1" , help = "The host of redirect_uri" )
322
340
p .add_argument ('--scope' , default = None , help = "The scope list" )
323
341
args = parser .parse_args ()
@@ -331,8 +349,8 @@ def __exit__(self, exc_type, exc_val, exc_tb):
331
349
auth_uri = flow ["auth_uri" ],
332
350
welcome_template =
333
351
"<a href='$auth_uri'>Sign In</a>, or <a href='$abort_uri'>Abort</a" ,
334
- error_template = "Oh no. $error" ,
352
+ error_template = "<html> Oh no. $error</html> " ,
335
353
success_template = "Oh yeah. Got $code" ,
336
- timeout = 60 ,
354
+ timeout = args . timeout ,
337
355
state = flow ["state" ], # Optional
338
356
), indent = 4 ))
0 commit comments