comparison CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/wsgiref/handlers.py @ 68:5028fdace37b

planemo upload commit 2e9511a184a1ca667c7be0c6321a36dc4e3d116d
author jpayne
date Tue, 18 Mar 2025 16:23:26 -0400
parents
children
comparison
equal deleted inserted replaced
67:0e9998148a16 68:5028fdace37b
1 """Base classes for server/gateway implementations"""
2
3 from .util import FileWrapper, guess_scheme, is_hop_by_hop
4 from .headers import Headers
5
6 import sys, os, time
7
8 __all__ = [
9 'BaseHandler', 'SimpleHandler', 'BaseCGIHandler', 'CGIHandler',
10 'IISCGIHandler', 'read_environ'
11 ]
12
13 # Weekday and month names for HTTP date/time formatting; always English!
14 _weekdayname = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
15 _monthname = [None, # Dummy so we can use 1-based month numbers
16 "Jan", "Feb", "Mar", "Apr", "May", "Jun",
17 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
18
19 def format_date_time(timestamp):
20 year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp)
21 return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
22 _weekdayname[wd], day, _monthname[month], year, hh, mm, ss
23 )
24
25 _is_request = {
26 'SCRIPT_NAME', 'PATH_INFO', 'QUERY_STRING', 'REQUEST_METHOD', 'AUTH_TYPE',
27 'CONTENT_TYPE', 'CONTENT_LENGTH', 'HTTPS', 'REMOTE_USER', 'REMOTE_IDENT',
28 }.__contains__
29
30 def _needs_transcode(k):
31 return _is_request(k) or k.startswith('HTTP_') or k.startswith('SSL_') \
32 or (k.startswith('REDIRECT_') and _needs_transcode(k[9:]))
33
34 def read_environ():
35 """Read environment, fixing HTTP variables"""
36 enc = sys.getfilesystemencoding()
37 esc = 'surrogateescape'
38 try:
39 ''.encode('utf-8', esc)
40 except LookupError:
41 esc = 'replace'
42 environ = {}
43
44 # Take the basic environment from native-unicode os.environ. Attempt to
45 # fix up the variables that come from the HTTP request to compensate for
46 # the bytes->unicode decoding step that will already have taken place.
47 for k, v in os.environ.items():
48 if _needs_transcode(k):
49
50 # On win32, the os.environ is natively Unicode. Different servers
51 # decode the request bytes using different encodings.
52 if sys.platform == 'win32':
53 software = os.environ.get('SERVER_SOFTWARE', '').lower()
54
55 # On IIS, the HTTP request will be decoded as UTF-8 as long
56 # as the input is a valid UTF-8 sequence. Otherwise it is
57 # decoded using the system code page (mbcs), with no way to
58 # detect this has happened. Because UTF-8 is the more likely
59 # encoding, and mbcs is inherently unreliable (an mbcs string
60 # that happens to be valid UTF-8 will not be decoded as mbcs)
61 # always recreate the original bytes as UTF-8.
62 if software.startswith('microsoft-iis/'):
63 v = v.encode('utf-8').decode('iso-8859-1')
64
65 # Apache mod_cgi writes bytes-as-unicode (as if ISO-8859-1) direct
66 # to the Unicode environ. No modification needed.
67 elif software.startswith('apache/'):
68 pass
69
70 # Python 3's http.server.CGIHTTPRequestHandler decodes
71 # using the urllib.unquote default of UTF-8, amongst other
72 # issues.
73 elif (
74 software.startswith('simplehttp/')
75 and 'python/3' in software
76 ):
77 v = v.encode('utf-8').decode('iso-8859-1')
78
79 # For other servers, guess that they have written bytes to
80 # the environ using stdio byte-oriented interfaces, ending up
81 # with the system code page.
82 else:
83 v = v.encode(enc, 'replace').decode('iso-8859-1')
84
85 # Recover bytes from unicode environ, using surrogate escapes
86 # where available (Python 3.1+).
87 else:
88 v = v.encode(enc, esc).decode('iso-8859-1')
89
90 environ[k] = v
91 return environ
92
93
94 class BaseHandler:
95 """Manage the invocation of a WSGI application"""
96
97 # Configuration parameters; can override per-subclass or per-instance
98 wsgi_version = (1,0)
99 wsgi_multithread = True
100 wsgi_multiprocess = True
101 wsgi_run_once = False
102
103 origin_server = True # We are transmitting direct to client
104 http_version = "1.0" # Version that should be used for response
105 server_software = None # String name of server software, if any
106
107 # os_environ is used to supply configuration from the OS environment:
108 # by default it's a copy of 'os.environ' as of import time, but you can
109 # override this in e.g. your __init__ method.
110 os_environ= read_environ()
111
112 # Collaborator classes
113 wsgi_file_wrapper = FileWrapper # set to None to disable
114 headers_class = Headers # must be a Headers-like class
115
116 # Error handling (also per-subclass or per-instance)
117 traceback_limit = None # Print entire traceback to self.get_stderr()
118 error_status = "500 Internal Server Error"
119 error_headers = [('Content-Type','text/plain')]
120 error_body = b"A server error occurred. Please contact the administrator."
121
122 # State variables (don't mess with these)
123 status = result = None
124 headers_sent = False
125 headers = None
126 bytes_sent = 0
127
128 def run(self, application):
129 """Invoke the application"""
130 # Note to self: don't move the close()! Asynchronous servers shouldn't
131 # call close() from finish_response(), so if you close() anywhere but
132 # the double-error branch here, you'll break asynchronous servers by
133 # prematurely closing. Async servers must return from 'run()' without
134 # closing if there might still be output to iterate over.
135 try:
136 self.setup_environ()
137 self.result = application(self.environ, self.start_response)
138 self.finish_response()
139 except (ConnectionAbortedError, BrokenPipeError, ConnectionResetError):
140 # We expect the client to close the connection abruptly from time
141 # to time.
142 return
143 except:
144 try:
145 self.handle_error()
146 except:
147 # If we get an error handling an error, just give up already!
148 self.close()
149 raise # ...and let the actual server figure it out.
150
151
152 def setup_environ(self):
153 """Set up the environment for one request"""
154
155 env = self.environ = self.os_environ.copy()
156 self.add_cgi_vars()
157
158 env['wsgi.input'] = self.get_stdin()
159 env['wsgi.errors'] = self.get_stderr()
160 env['wsgi.version'] = self.wsgi_version
161 env['wsgi.run_once'] = self.wsgi_run_once
162 env['wsgi.url_scheme'] = self.get_scheme()
163 env['wsgi.multithread'] = self.wsgi_multithread
164 env['wsgi.multiprocess'] = self.wsgi_multiprocess
165
166 if self.wsgi_file_wrapper is not None:
167 env['wsgi.file_wrapper'] = self.wsgi_file_wrapper
168
169 if self.origin_server and self.server_software:
170 env.setdefault('SERVER_SOFTWARE',self.server_software)
171
172
173 def finish_response(self):
174 """Send any iterable data, then close self and the iterable
175
176 Subclasses intended for use in asynchronous servers will
177 want to redefine this method, such that it sets up callbacks
178 in the event loop to iterate over the data, and to call
179 'self.close()' once the response is finished.
180 """
181 try:
182 if not self.result_is_file() or not self.sendfile():
183 for data in self.result:
184 self.write(data)
185 self.finish_content()
186 except:
187 # Call close() on the iterable returned by the WSGI application
188 # in case of an exception.
189 if hasattr(self.result, 'close'):
190 self.result.close()
191 raise
192 else:
193 # We only call close() when no exception is raised, because it
194 # will set status, result, headers, and environ fields to None.
195 # See bpo-29183 for more details.
196 self.close()
197
198
199 def get_scheme(self):
200 """Return the URL scheme being used"""
201 return guess_scheme(self.environ)
202
203
204 def set_content_length(self):
205 """Compute Content-Length or switch to chunked encoding if possible"""
206 try:
207 blocks = len(self.result)
208 except (TypeError,AttributeError,NotImplementedError):
209 pass
210 else:
211 if blocks==1:
212 self.headers['Content-Length'] = str(self.bytes_sent)
213 return
214 # XXX Try for chunked encoding if origin server and client is 1.1
215
216
217 def cleanup_headers(self):
218 """Make any necessary header changes or defaults
219
220 Subclasses can extend this to add other defaults.
221 """
222 if 'Content-Length' not in self.headers:
223 self.set_content_length()
224
225 def start_response(self, status, headers,exc_info=None):
226 """'start_response()' callable as specified by PEP 3333"""
227
228 if exc_info:
229 try:
230 if self.headers_sent:
231 # Re-raise original exception if headers sent
232 raise exc_info[0](exc_info[1]).with_traceback(exc_info[2])
233 finally:
234 exc_info = None # avoid dangling circular ref
235 elif self.headers is not None:
236 raise AssertionError("Headers already set!")
237
238 self.status = status
239 self.headers = self.headers_class(headers)
240 status = self._convert_string_type(status, "Status")
241 assert len(status)>=4,"Status must be at least 4 characters"
242 assert status[:3].isdigit(), "Status message must begin w/3-digit code"
243 assert status[3]==" ", "Status message must have a space after code"
244
245 if __debug__:
246 for name, val in headers:
247 name = self._convert_string_type(name, "Header name")
248 val = self._convert_string_type(val, "Header value")
249 assert not is_hop_by_hop(name),\
250 f"Hop-by-hop header, '{name}: {val}', not allowed"
251
252 return self.write
253
254 def _convert_string_type(self, value, title):
255 """Convert/check value type."""
256 if type(value) is str:
257 return value
258 raise AssertionError(
259 "{0} must be of type str (got {1})".format(title, repr(value))
260 )
261
262 def send_preamble(self):
263 """Transmit version/status/date/server, via self._write()"""
264 if self.origin_server:
265 if self.client_is_modern():
266 self._write(('HTTP/%s %s\r\n' % (self.http_version,self.status)).encode('iso-8859-1'))
267 if 'Date' not in self.headers:
268 self._write(
269 ('Date: %s\r\n' % format_date_time(time.time())).encode('iso-8859-1')
270 )
271 if self.server_software and 'Server' not in self.headers:
272 self._write(('Server: %s\r\n' % self.server_software).encode('iso-8859-1'))
273 else:
274 self._write(('Status: %s\r\n' % self.status).encode('iso-8859-1'))
275
276 def write(self, data):
277 """'write()' callable as specified by PEP 3333"""
278
279 assert type(data) is bytes, \
280 "write() argument must be a bytes instance"
281
282 if not self.status:
283 raise AssertionError("write() before start_response()")
284
285 elif not self.headers_sent:
286 # Before the first output, send the stored headers
287 self.bytes_sent = len(data) # make sure we know content-length
288 self.send_headers()
289 else:
290 self.bytes_sent += len(data)
291
292 # XXX check Content-Length and truncate if too many bytes written?
293 self._write(data)
294 self._flush()
295
296
297 def sendfile(self):
298 """Platform-specific file transmission
299
300 Override this method in subclasses to support platform-specific
301 file transmission. It is only called if the application's
302 return iterable ('self.result') is an instance of
303 'self.wsgi_file_wrapper'.
304
305 This method should return a true value if it was able to actually
306 transmit the wrapped file-like object using a platform-specific
307 approach. It should return a false value if normal iteration
308 should be used instead. An exception can be raised to indicate
309 that transmission was attempted, but failed.
310
311 NOTE: this method should call 'self.send_headers()' if
312 'self.headers_sent' is false and it is going to attempt direct
313 transmission of the file.
314 """
315 return False # No platform-specific transmission by default
316
317
318 def finish_content(self):
319 """Ensure headers and content have both been sent"""
320 if not self.headers_sent:
321 # Only zero Content-Length if not set by the application (so
322 # that HEAD requests can be satisfied properly, see #3839)
323 self.headers.setdefault('Content-Length', "0")
324 self.send_headers()
325 else:
326 pass # XXX check if content-length was too short?
327
328 def close(self):
329 """Close the iterable (if needed) and reset all instance vars
330
331 Subclasses may want to also drop the client connection.
332 """
333 try:
334 if hasattr(self.result,'close'):
335 self.result.close()
336 finally:
337 self.result = self.headers = self.status = self.environ = None
338 self.bytes_sent = 0; self.headers_sent = False
339
340
341 def send_headers(self):
342 """Transmit headers to the client, via self._write()"""
343 self.cleanup_headers()
344 self.headers_sent = True
345 if not self.origin_server or self.client_is_modern():
346 self.send_preamble()
347 self._write(bytes(self.headers))
348
349
350 def result_is_file(self):
351 """True if 'self.result' is an instance of 'self.wsgi_file_wrapper'"""
352 wrapper = self.wsgi_file_wrapper
353 return wrapper is not None and isinstance(self.result,wrapper)
354
355
356 def client_is_modern(self):
357 """True if client can accept status and headers"""
358 return self.environ['SERVER_PROTOCOL'].upper() != 'HTTP/0.9'
359
360
361 def log_exception(self,exc_info):
362 """Log the 'exc_info' tuple in the server log
363
364 Subclasses may override to retarget the output or change its format.
365 """
366 try:
367 from traceback import print_exception
368 stderr = self.get_stderr()
369 print_exception(
370 exc_info[0], exc_info[1], exc_info[2],
371 self.traceback_limit, stderr
372 )
373 stderr.flush()
374 finally:
375 exc_info = None
376
377 def handle_error(self):
378 """Log current error, and send error output to client if possible"""
379 self.log_exception(sys.exc_info())
380 if not self.headers_sent:
381 self.result = self.error_output(self.environ, self.start_response)
382 self.finish_response()
383 # XXX else: attempt advanced recovery techniques for HTML or text?
384
385 def error_output(self, environ, start_response):
386 """WSGI mini-app to create error output
387
388 By default, this just uses the 'error_status', 'error_headers',
389 and 'error_body' attributes to generate an output page. It can
390 be overridden in a subclass to dynamically generate diagnostics,
391 choose an appropriate message for the user's preferred language, etc.
392
393 Note, however, that it's not recommended from a security perspective to
394 spit out diagnostics to any old user; ideally, you should have to do
395 something special to enable diagnostic output, which is why we don't
396 include any here!
397 """
398 start_response(self.error_status,self.error_headers[:],sys.exc_info())
399 return [self.error_body]
400
401
402 # Pure abstract methods; *must* be overridden in subclasses
403
404 def _write(self,data):
405 """Override in subclass to buffer data for send to client
406
407 It's okay if this method actually transmits the data; BaseHandler
408 just separates write and flush operations for greater efficiency
409 when the underlying system actually has such a distinction.
410 """
411 raise NotImplementedError
412
413 def _flush(self):
414 """Override in subclass to force sending of recent '_write()' calls
415
416 It's okay if this method is a no-op (i.e., if '_write()' actually
417 sends the data.
418 """
419 raise NotImplementedError
420
421 def get_stdin(self):
422 """Override in subclass to return suitable 'wsgi.input'"""
423 raise NotImplementedError
424
425 def get_stderr(self):
426 """Override in subclass to return suitable 'wsgi.errors'"""
427 raise NotImplementedError
428
429 def add_cgi_vars(self):
430 """Override in subclass to insert CGI variables in 'self.environ'"""
431 raise NotImplementedError
432
433
434 class SimpleHandler(BaseHandler):
435 """Handler that's just initialized with streams, environment, etc.
436
437 This handler subclass is intended for synchronous HTTP/1.0 origin servers,
438 and handles sending the entire response output, given the correct inputs.
439
440 Usage::
441
442 handler = SimpleHandler(
443 inp,out,err,env, multithread=False, multiprocess=True
444 )
445 handler.run(app)"""
446
447 def __init__(self,stdin,stdout,stderr,environ,
448 multithread=True, multiprocess=False
449 ):
450 self.stdin = stdin
451 self.stdout = stdout
452 self.stderr = stderr
453 self.base_env = environ
454 self.wsgi_multithread = multithread
455 self.wsgi_multiprocess = multiprocess
456
457 def get_stdin(self):
458 return self.stdin
459
460 def get_stderr(self):
461 return self.stderr
462
463 def add_cgi_vars(self):
464 self.environ.update(self.base_env)
465
466 def _write(self,data):
467 result = self.stdout.write(data)
468 if result is None or result == len(data):
469 return
470 from warnings import warn
471 warn("SimpleHandler.stdout.write() should not do partial writes",
472 DeprecationWarning)
473 while True:
474 data = data[result:]
475 if not data:
476 break
477 result = self.stdout.write(data)
478
479 def _flush(self):
480 self.stdout.flush()
481 self._flush = self.stdout.flush
482
483
484 class BaseCGIHandler(SimpleHandler):
485
486 """CGI-like systems using input/output/error streams and environ mapping
487
488 Usage::
489
490 handler = BaseCGIHandler(inp,out,err,env)
491 handler.run(app)
492
493 This handler class is useful for gateway protocols like ReadyExec and
494 FastCGI, that have usable input/output/error streams and an environment
495 mapping. It's also the base class for CGIHandler, which just uses
496 sys.stdin, os.environ, and so on.
497
498 The constructor also takes keyword arguments 'multithread' and
499 'multiprocess' (defaulting to 'True' and 'False' respectively) to control
500 the configuration sent to the application. It sets 'origin_server' to
501 False (to enable CGI-like output), and assumes that 'wsgi.run_once' is
502 False.
503 """
504
505 origin_server = False
506
507
508 class CGIHandler(BaseCGIHandler):
509
510 """CGI-based invocation via sys.stdin/stdout/stderr and os.environ
511
512 Usage::
513
514 CGIHandler().run(app)
515
516 The difference between this class and BaseCGIHandler is that it always
517 uses 'wsgi.run_once' of 'True', 'wsgi.multithread' of 'False', and
518 'wsgi.multiprocess' of 'True'. It does not take any initialization
519 parameters, but always uses 'sys.stdin', 'os.environ', and friends.
520
521 If you need to override any of these parameters, use BaseCGIHandler
522 instead.
523 """
524
525 wsgi_run_once = True
526 # Do not allow os.environ to leak between requests in Google App Engine
527 # and other multi-run CGI use cases. This is not easily testable.
528 # See http://bugs.python.org/issue7250
529 os_environ = {}
530
531 def __init__(self):
532 BaseCGIHandler.__init__(
533 self, sys.stdin.buffer, sys.stdout.buffer, sys.stderr,
534 read_environ(), multithread=False, multiprocess=True
535 )
536
537
538 class IISCGIHandler(BaseCGIHandler):
539 """CGI-based invocation with workaround for IIS path bug
540
541 This handler should be used in preference to CGIHandler when deploying on
542 Microsoft IIS without having set the config allowPathInfo option (IIS>=7)
543 or metabase allowPathInfoForScriptMappings (IIS<7).
544 """
545 wsgi_run_once = True
546 os_environ = {}
547
548 # By default, IIS gives a PATH_INFO that duplicates the SCRIPT_NAME at
549 # the front, causing problems for WSGI applications that wish to implement
550 # routing. This handler strips any such duplicated path.
551
552 # IIS can be configured to pass the correct PATH_INFO, but this causes
553 # another bug where PATH_TRANSLATED is wrong. Luckily this variable is
554 # rarely used and is not guaranteed by WSGI. On IIS<7, though, the
555 # setting can only be made on a vhost level, affecting all other script
556 # mappings, many of which break when exposed to the PATH_TRANSLATED bug.
557 # For this reason IIS<7 is almost never deployed with the fix. (Even IIS7
558 # rarely uses it because there is still no UI for it.)
559
560 # There is no way for CGI code to tell whether the option was set, so a
561 # separate handler class is provided.
562 def __init__(self):
563 environ= read_environ()
564 path = environ.get('PATH_INFO', '')
565 script = environ.get('SCRIPT_NAME', '')
566 if (path+'/').startswith(script+'/'):
567 environ['PATH_INFO'] = path[len(script):]
568 BaseCGIHandler.__init__(
569 self, sys.stdin.buffer, sys.stdout.buffer, sys.stderr,
570 environ, multithread=False, multiprocess=True
571 )