| 1 | # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
|
|---|
| 2 | # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
|
|---|
| 3 | # Also licenced under the Apache License, 2.0: http://opensource.org/licenses/apache2.0.php
|
|---|
| 4 | # Licensed to PSF under a Contributor Agreement
|
|---|
| 5 | """
|
|---|
| 6 | Middleware to check for obedience to the WSGI specification.
|
|---|
| 7 |
|
|---|
| 8 | Some of the things this checks:
|
|---|
| 9 |
|
|---|
| 10 | * Signature of the application and start_response (including that
|
|---|
| 11 | keyword arguments are not used).
|
|---|
| 12 |
|
|---|
| 13 | * Environment checks:
|
|---|
| 14 |
|
|---|
| 15 | - Environment is a dictionary (and not a subclass).
|
|---|
| 16 |
|
|---|
| 17 | - That all the required keys are in the environment: REQUEST_METHOD,
|
|---|
| 18 | SERVER_NAME, SERVER_PORT, wsgi.version, wsgi.input, wsgi.errors,
|
|---|
| 19 | wsgi.multithread, wsgi.multiprocess, wsgi.run_once
|
|---|
| 20 |
|
|---|
| 21 | - That HTTP_CONTENT_TYPE and HTTP_CONTENT_LENGTH are not in the
|
|---|
| 22 | environment (these headers should appear as CONTENT_LENGTH and
|
|---|
| 23 | CONTENT_TYPE).
|
|---|
| 24 |
|
|---|
| 25 | - Warns if QUERY_STRING is missing, as the cgi module acts
|
|---|
| 26 | unpredictably in that case.
|
|---|
| 27 |
|
|---|
| 28 | - That CGI-style variables (that don't contain a .) have
|
|---|
| 29 | (non-unicode) string values
|
|---|
| 30 |
|
|---|
| 31 | - That wsgi.version is a tuple
|
|---|
| 32 |
|
|---|
| 33 | - That wsgi.url_scheme is 'http' or 'https' (@@: is this too
|
|---|
| 34 | restrictive?)
|
|---|
| 35 |
|
|---|
| 36 | - Warns if the REQUEST_METHOD is not known (@@: probably too
|
|---|
| 37 | restrictive).
|
|---|
| 38 |
|
|---|
| 39 | - That SCRIPT_NAME and PATH_INFO are empty or start with /
|
|---|
| 40 |
|
|---|
| 41 | - That at least one of SCRIPT_NAME or PATH_INFO are set.
|
|---|
| 42 |
|
|---|
| 43 | - That CONTENT_LENGTH is a positive integer.
|
|---|
| 44 |
|
|---|
| 45 | - That SCRIPT_NAME is not '/' (it should be '', and PATH_INFO should
|
|---|
| 46 | be '/').
|
|---|
| 47 |
|
|---|
| 48 | - That wsgi.input has the methods read, readline, readlines, and
|
|---|
| 49 | __iter__
|
|---|
| 50 |
|
|---|
| 51 | - That wsgi.errors has the methods flush, write, writelines
|
|---|
| 52 |
|
|---|
| 53 | * The status is a string, contains a space, starts with an integer,
|
|---|
| 54 | and that integer is in range (> 100).
|
|---|
| 55 |
|
|---|
| 56 | * That the headers is a list (not a subclass, not another kind of
|
|---|
| 57 | sequence).
|
|---|
| 58 |
|
|---|
| 59 | * That the items of the headers are tuples of strings.
|
|---|
| 60 |
|
|---|
| 61 | * That there is no 'status' header (that is used in CGI, but not in
|
|---|
| 62 | WSGI).
|
|---|
| 63 |
|
|---|
| 64 | * That the headers don't contain newlines or colons, end in _ or -, or
|
|---|
| 65 | contain characters codes below 037.
|
|---|
| 66 |
|
|---|
| 67 | * That Content-Type is given if there is content (CGI often has a
|
|---|
| 68 | default content type, but WSGI does not).
|
|---|
| 69 |
|
|---|
| 70 | * That no Content-Type is given when there is no content (@@: is this
|
|---|
| 71 | too restrictive?)
|
|---|
| 72 |
|
|---|
| 73 | * That the exc_info argument to start_response is a tuple or None.
|
|---|
| 74 |
|
|---|
| 75 | * That all calls to the writer are with strings, and no other methods
|
|---|
| 76 | on the writer are accessed.
|
|---|
| 77 |
|
|---|
| 78 | * That wsgi.input is used properly:
|
|---|
| 79 |
|
|---|
| 80 | - .read() is called with zero or one argument
|
|---|
| 81 |
|
|---|
| 82 | - That it returns a string
|
|---|
| 83 |
|
|---|
| 84 | - That readline, readlines, and __iter__ return strings
|
|---|
| 85 |
|
|---|
| 86 | - That .close() is not called
|
|---|
| 87 |
|
|---|
| 88 | - No other methods are provided
|
|---|
| 89 |
|
|---|
| 90 | * That wsgi.errors is used properly:
|
|---|
| 91 |
|
|---|
| 92 | - .write() and .writelines() is called with a string
|
|---|
| 93 |
|
|---|
| 94 | - That .close() is not called, and no other methods are provided.
|
|---|
| 95 |
|
|---|
| 96 | * The response iterator:
|
|---|
| 97 |
|
|---|
| 98 | - That it is not a string (it should be a list of a single string; a
|
|---|
| 99 | string will work, but perform horribly).
|
|---|
| 100 |
|
|---|
| 101 | - That .next() returns a string
|
|---|
| 102 |
|
|---|
| 103 | - That the iterator is not iterated over until start_response has
|
|---|
| 104 | been called (that can signal either a server or application
|
|---|
| 105 | error).
|
|---|
| 106 |
|
|---|
| 107 | - That .close() is called (doesn't raise exception, only prints to
|
|---|
| 108 | sys.stderr, because we only know it isn't called when the object
|
|---|
| 109 | is garbage collected).
|
|---|
| 110 | """
|
|---|
| 111 | __all__ = ['validator']
|
|---|
| 112 |
|
|---|
| 113 |
|
|---|
| 114 | import re
|
|---|
| 115 | import sys
|
|---|
| 116 | from types import DictType, StringType, TupleType, ListType
|
|---|
| 117 | import warnings
|
|---|
| 118 |
|
|---|
| 119 | header_re = re.compile(r'^[a-zA-Z][a-zA-Z0-9\-_]*$')
|
|---|
| 120 | bad_header_value_re = re.compile(r'[\000-\037]')
|
|---|
| 121 |
|
|---|
| 122 | class WSGIWarning(Warning):
|
|---|
| 123 | """
|
|---|
| 124 | Raised in response to WSGI-spec-related warnings
|
|---|
| 125 | """
|
|---|
| 126 |
|
|---|
| 127 | def assert_(cond, *args):
|
|---|
| 128 | if not cond:
|
|---|
| 129 | raise AssertionError(*args)
|
|---|
| 130 |
|
|---|
| 131 | def validator(application):
|
|---|
| 132 |
|
|---|
| 133 | """
|
|---|
| 134 | When applied between a WSGI server and a WSGI application, this
|
|---|
| 135 | middleware will check for WSGI compliancy on a number of levels.
|
|---|
|
|---|