Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

local WSGI Request and Response classes

This change replaces WebOb with a mostly compatible local library,
swift.common.swob.  Subtle changes to WebOb's API over the years have been a
huge headache.  Swift doesn't even run on the current version.

There are a few incompatibilities to simplify the implementation/interface:
 * It only implements the header properties we use.  More can be easily added.
 * Casts header values to str on assignment.
 * Response classes ("HTTPNotFound") are no longer subclasses, but partials
   on Response, so things like isinstance no longer work on them.
 * Unlike newer webob versions, will never return unicode objects.

Change-Id: I76617a0903ee2286b25a821b3c935c86ff95233f
  • Loading branch information...
commit 5e3e9a882de8b51b8e3b27628ba39f0dabfc78df 1 parent f0bd91d
Michael Barton authored September 04, 2012

Showing 54 changed files with 1,448 additions and 225 deletions. Show diff stats Hide diff stats

  1. 4  doc/source/debian_package_guide.rst
  2. 16  doc/source/development_auth.rst
  3. 4  doc/source/development_saio.rst
  4. 1  doc/source/getting_started.rst
  5. 11  swift/account/server.py
  6. 2  swift/common/constraints.py
  7. 10  swift/common/db_replicator.py
  8. 34  swift/common/http.py
  9. 3  swift/common/internal_client.py
  10. 3  swift/common/middleware/catch_errors.py
  11. 3  swift/common/middleware/cname_lookup.py
  12. 3  swift/common/middleware/domain_remap.py
  13. 2  swift/common/middleware/healthcheck.py
  14. 11  swift/common/middleware/keystoneauth.py
  15. 5  swift/common/middleware/name_check.py
  16. 3  swift/common/middleware/proxy_logging.py
  17. 4  swift/common/middleware/ratelimit.py
  18. 2  swift/common/middleware/recon.py
  19. 3  swift/common/middleware/staticweb.py
  20. 14  swift/common/middleware/tempauth.py
  21. 840  swift/common/swob.py
  22. 25  swift/common/utils.py
  23. 6  swift/common/wsgi.py
  24. 11  swift/container/server.py
  25. 13  swift/obj/server.py
  26. 4  swift/proxy/controllers/account.py
  27. 15  swift/proxy/controllers/base.py
  28. 4  swift/proxy/controllers/container.py
  29. 18  swift/proxy/controllers/obj.py
  30. 13  swift/proxy/server.py
  31. 6  test/functional/tests.py
  32. 20  test/unit/account/test_server.py
  33. 3  test/unit/common/middleware/test_cname_lookup.py
  34. 3  test/unit/common/middleware/test_domain_remap.py
  35. 3  test/unit/common/middleware/test_except.py
  36. 3  test/unit/common/middleware/test_formpost.py
  37. 3  test/unit/common/middleware/test_healthcheck.py
  38. 25  test/unit/common/middleware/test_keystoneauth.py
  39. 4  test/unit/common/middleware/test_memcache.py
  40. 3  test/unit/common/middleware/test_name_check.py
  41. 3  test/unit/common/middleware/test_proxy_logging.py
  42. 2  test/unit/common/middleware/test_ratelimit.py
  43. 5  test/unit/common/middleware/test_recon.py
  44. 3  test/unit/common/middleware/test_staticweb.py
  45. 3  test/unit/common/middleware/test_tempauth.py
  46. 3  test/unit/common/middleware/test_tempurl.py
  47. 60  test/unit/common/test_constraints.py
  48. 398  test/unit/common/test_swob.py
  49. 2  test/unit/common/test_wsgi.py
  50. 2  test/unit/container/test_server.py
  51. 2  test/unit/obj/test_internal_client.py
  52. 24  test/unit/obj/test_server.py
  53. 5  test/unit/proxy/test_server.py
  54. 1  tools/pip-requires
4  doc/source/debian_package_guide.rst
Source Rendered
@@ -58,7 +58,7 @@ Instructions for Building Debian Packages for Swift
58 58
        apt-get install python-software-properties
59 59
        add-apt-repository ppa:swift-core/release
60 60
        apt-get update
61  
-       apt-get install curl gcc bzr python-configobj python-coverage python-dev python-nose python-setuptools python-simplejson python-xattr python-webob python-eventlet python-greenlet debhelper python-sphinx python-all python-openssl python-pastedeploy python-netifaces bzr-builddeb
  61
+       apt-get install curl gcc bzr python-configobj python-coverage python-dev python-nose python-setuptools python-simplejson python-xattr python-eventlet python-greenlet debhelper python-sphinx python-all python-openssl python-pastedeploy python-netifaces bzr-builddeb
62 62
 
63 63
 * As you
64 64
 
@@ -105,7 +105,7 @@ Instructions for Deploying Debian Packages for Swift
105 105
 
106 106
   #. Install dependencies::
107 107
  
108  
-       apt-get install rsync python-openssl python-setuptools python-webob
  108
+       apt-get install rsync python-openssl python-setuptools
109 109
        python-simplejson python-xattr python-greenlet python-eventlet
110 110
        python-netifaces
111 111
 
16  doc/source/development_auth.rst
Source Rendered
@@ -63,7 +63,7 @@ Example Authentication with TempAuth:
63 63
 
64 64
 Authorization is performed through callbacks by the Swift Proxy server to the
65 65
 WSGI environment's swift.authorize value, if one is set. The swift.authorize
66  
-value should simply be a function that takes a webob.Request as an argument and
  66
+value should simply be a function that takes a Request as an argument and
67 67
 returns None if access is granted or returns a callable(environ,
68 68
 start_response) if access is denied. This callable is a standard WSGI callable.
69 69
 Generally, you should return 403 Forbidden for requests by an authenticated
@@ -71,7 +71,7 @@ user and 401 Unauthorized for an unauthenticated request. For example, here's
71 71
 an authorize function that only allows GETs (in this case you'd probably return
72 72
 405 Method Not Allowed, but ignore that for the moment).::
73 73
 
74  
-    from webob.exc import HTTPForbidden, HTTPUnauthorized
  74
+    from swift.common.swob import HTTPForbidden, HTTPUnauthorized
75 75
 
76 76
 
77 77
     def authorize(req):
@@ -87,7 +87,7 @@ middleware as authentication and authorization are often paired together. But,
87 87
 you could create separate authorization middleware that simply sets the
88 88
 callback before passing on the request. To continue our example above::
89 89
 
90  
-    from webob.exc import HTTPForbidden, HTTPUnauthorized
  90
+    from swift.common.swob import HTTPForbidden, HTTPUnauthorized
91 91
 
92 92
 
93 93
     class Authorization(object):
@@ -127,7 +127,7 @@ then swift.authorize will be called once more. These are called delay_denial
127 127
 requests and currently include container read requests and object read and
128 128
 write requests. For these requests, the read or write access control string
129 129
 (X-Container-Read and X-Container-Write) will be fetched and set as the 'acl'
130  
-attribute in the webob.Request passed to swift.authorize.
  130
+attribute in the Request passed to swift.authorize.
131 131
 
132 132
 The delay_denial procedures allow skipping possibly expensive access control
133 133
 string retrievals for requests that can be approved without that information,
@@ -138,7 +138,7 @@ control string set to same value as the authenticated user string. Note that
138 138
 you probably wouldn't do this exactly as the access control string represents a
139 139
 list rather than a single user, but it'll suffice for this example::
140 140
 
141  
-    from webob.exc import HTTPForbidden, HTTPUnauthorized
  141
+    from swift.common.swob import HTTPForbidden, HTTPUnauthorized
142 142
 
143 143
 
144 144
     class Authorization(object):
@@ -185,7 +185,7 @@ Let's continue our example to use parse_acl and referrer_allowed. Now we'll
185 185
 only allow GETs after a referrer check and any requests after a group check::
186 186
 
187 187
     from swift.common.middleware.acl import parse_acl, referrer_allowed
188  
-    from webob.exc import HTTPForbidden, HTTPUnauthorized
  188
+    from swift.common.swob import HTTPForbidden, HTTPUnauthorized
189 189
 
190 190
 
191 191
     class Authorization(object):
@@ -235,7 +235,7 @@ standard Swift format. Let's improve our example by making use of that::
235 235
 
236 236
     from swift.common.middleware.acl import \
237 237
         clean_acl, parse_acl, referrer_allowed
238  
-    from webob.exc import HTTPForbidden, HTTPUnauthorized
  238
+    from swift.common.swob import HTTPForbidden, HTTPUnauthorized
239 239
 
240 240
 
241 241
     class Authorization(object):
@@ -293,7 +293,7 @@ folks a start on their own code if they want to use repoze.what::
293 293
     from swift.common.bufferedhttp import http_connect_raw as http_connect
294 294
     from swift.common.middleware.acl import clean_acl, parse_acl, referrer_allowed
295 295
     from swift.common.utils import cache_from_env, split_path
296  
-    from webob.exc import HTTPForbidden, HTTPUnauthorized
  296
+    from swift.common.swob import HTTPForbidden, HTTPUnauthorized
297 297
 
298 298
 
299 299
     class DevAuthorization(object):
4  doc/source/development_saio.rst
Source Rendered
@@ -30,8 +30,8 @@ Installing dependencies and the core code
30 30
   #. `apt-get update`
31 31
   #. `apt-get install curl gcc git-core memcached python-configobj
32 32
      python-coverage python-dev python-nose python-setuptools python-simplejson
33  
-     python-xattr sqlite3 xfsprogs python-webob python-eventlet
34  
-     python-greenlet python-pastedeploy python-netifaces python-pip`
  33
+     python-xattr sqlite3 xfsprogs python-eventlet python-greenlet
  34
+     python-pastedeploy python-netifaces python-pip`
35 35
   #. `pip install mock`
36 36
   #. Install anything else you want, like screen, ssh, vim, etc.
37 37
 
1  doc/source/getting_started.rst
Source Rendered
@@ -15,7 +15,6 @@ most Linux platforms with the following software:
15 15
 And the following python libraries:
16 16
 
17 17
 * Eventlet 0.9.8
18  
-* WebOb 0.9.8
19 18
 * Setuptools
20 19
 * Simplejson
21 20
 * Xattr
11  swift/account/server.py
@@ -22,11 +22,6 @@
22 22
 from xml.sax import saxutils
23 23
 
24 24
 from eventlet import Timeout
25  
-from webob import Request, Response
26  
-from webob.exc import HTTPAccepted, HTTPBadRequest, \
27  
-    HTTPCreated, HTTPForbidden, HTTPInternalServerError, \
28  
-    HTTPMethodNotAllowed, HTTPNoContent, HTTPNotFound, \
29  
-    HTTPPreconditionFailed, HTTPConflict
30 25
 
31 26
 import swift.common.db
32 27
 from swift.common.db import AccountBroker
@@ -36,7 +31,11 @@
36 31
 from swift.common.constraints import ACCOUNT_LISTING_LIMIT, \
37 32
     check_mount, check_float, check_utf8, FORMAT2CONTENT_TYPE
38 33
 from swift.common.db_replicator import ReplicatorRpc
39  
-from swift.common.http import HTTPInsufficientStorage
  34
+from swift.common.swob import HTTPAccepted, HTTPBadRequest, \
  35
+    HTTPCreated, HTTPForbidden, HTTPInternalServerError, \
  36
+    HTTPMethodNotAllowed, HTTPNoContent, HTTPNotFound, \
  37
+    HTTPPreconditionFailed, HTTPConflict, Request, Response, \
  38
+    HTTPInsufficientStorage
40 39
 
41 40
 
42 41
 DATADIR = 'accounts'
2  swift/common/constraints.py
@@ -17,7 +17,7 @@
17 17
 from ConfigParser import ConfigParser, NoSectionError, NoOptionError, \
18 18
     RawConfigParser
19 19
 
20  
-from webob.exc import HTTPBadRequest, HTTPLengthRequired, \
  20
+from swift.common.swob import HTTPBadRequest, HTTPLengthRequired, \
21 21
     HTTPRequestEntityTooLarge
22 22
 
23 23
 constraints_conf = ConfigParser()
10  swift/common/db_replicator.py
@@ -26,18 +26,18 @@
26 26
 from eventlet import GreenPool, sleep, Timeout
27 27
 from eventlet.green import subprocess
28 28
 import simplejson
29  
-from webob import Response
30  
-from webob.exc import HTTPNotFound, HTTPNoContent, HTTPAccepted, \
31  
-    HTTPInsufficientStorage, HTTPBadRequest
32 29
 
33 30
 import swift.common.db
34 31
 from swift.common.utils import get_logger, whataremyips, storage_directory, \
35 32
     renamer, mkdirs, lock_parent_directory, TRUE_VALUES, unlink_older_than, \
36 33
     dump_recon_cache, rsync_ip
37 34
 from swift.common import ring
  35
+from swift.common.http import HTTP_NOT_FOUND, HTTP_INSUFFICIENT_STORAGE
38 36
 from swift.common.bufferedhttp import BufferedHTTPConnection
39 37
 from swift.common.exceptions import DriveNotMounted, ConnectionTimeout
40 38
 from swift.common.daemon import Daemon
  39
+from swift.common.swob import Response, HTTPNotFound, HTTPNoContent, \
  40
+    HTTPAccepted, HTTPInsufficientStorage, HTTPBadRequest
41 41
 
42 42
 
43 43
 DEBUG_TIMINGS_THRESHOLD = 10
@@ -324,11 +324,11 @@ def _repl_to_node(self, node, broker, partition, info):
324 324
                 info['delete_timestamp'], info['metadata'])
325 325
         if not response:
326 326
             return False
327  
-        elif response.status == HTTPNotFound.code:  # completely missing, rsync
  327
+        elif response.status == HTTP_NOT_FOUND:  # completely missing, rsync
328 328
             self.stats['rsync'] += 1
329 329
             self.logger.increment('rsyncs')
330 330
             return self._rsync_db(broker, node, http, info['id'])
331  
-        elif response.status == HTTPInsufficientStorage.code:
  331
+        elif response.status == HTTP_INSUFFICIENT_STORAGE:
332 332
             raise DriveNotMounted()
333 333
         elif response.status >= 200 and response.status < 300:
334 334
             rinfo = simplejson.loads(response.data)
34  swift/common/http.py
@@ -13,40 +13,6 @@
13 13
 # See the License for the specific language governing permissions and
14 14
 # limitations under the License.
15 15
 
16  
-from webob.exc import HTTPClientError,\
17  
-    HTTPInsufficientStorage as BaseHTTPInsufficientStorage
18  
-
19  
-
20  
-class HTTPClientDisconnect(HTTPClientError):
21  
-    """
22  
-    subclass of :class:`~HTTPClientError`
23  
-
24  
-    This code is introduced to log the case when the connection is closed by
25  
-    client while HTTP server is processing its request
26  
-
27  
-    code: 499, title: Client Disconnect
28  
-    """
29  
-    code = 499
30  
-    title = 'Client Disconnect'
31  
-    explanation = (
32  
-        'This code is introduced to log the case when the connection '
33  
-        'is closed by client while HTTP server is processing its request')
34  
-
35  
-
36  
-class HTTPInsufficientStorage(BaseHTTPInsufficientStorage):
37  
-    """
38  
-    subclass of :class:`~HTTPInsufficientStorage`
39  
-
40  
-    The server is unable to store the representation needed to
41  
-    complete the request.
42  
-
43  
-    code: 507, title: Insufficient Storage
44  
-    """
45  
-    def __init__(self, drive=None, *args, **kwargs):
46  
-        if drive:
47  
-            self.explanation = ('%s is not mounted' % drive)
48  
-        super(HTTPInsufficientStorage, self).__init__(*args, **kwargs)
49  
-
50 16
 
51 17
 def is_informational(status):
52 18
     """
3  swift/common/internal_client.py
@@ -19,12 +19,11 @@
19 19
 import struct
20 20
 from sys import exc_info
21 21
 from urllib import quote
22  
-from webob import Request
23 22
 import zlib
24 23
 from zlib import compressobj
25 24
 
26  
-
27 25
 from swift.common.http import HTTP_NOT_FOUND
  26
+from swift.common.swob import Request
28 27
 
29 28
 
30 29
 class UnexpectedResponse(Exception):
3  swift/common/middleware/catch_errors.py
@@ -14,10 +14,9 @@
14 14
 # limitations under the License.
15 15
 
16 16
 from eventlet import Timeout
17  
-from webob import Request
18  
-from webob.exc import HTTPServerError
19 17
 import uuid
20 18
 
  19
+from swift.common.swob import Request, HTTPServerError
21 20
 from swift.common.utils import get_logger
22 21
 
23 22
 
3  swift/common/middleware/cname_lookup.py
@@ -27,8 +27,6 @@
27 27
 rewritten and the request is passed further down the WSGI chain.
28 28
 """
29 29
 
30  
-from webob import Request
31  
-from webob.exc import HTTPBadRequest
32 30
 try:
33 31
     import dns.resolver
34 32
     from dns.exception import DNSException
@@ -39,6 +37,7 @@
39 37
 else:  # executed if the try block finishes with no errors
40 38
     MODULE_DEPENDENCY_MET = True
41 39
 
  40
+from swift.common.swob import Request, HTTPBadRequest
42 41
 from swift.common.utils import cache_from_env, get_logger
43 42
 
44 43
 
3  swift/common/middleware/domain_remap.py
@@ -49,8 +49,7 @@
49 49
 sync destinations.
50 50
 """
51 51
 
52  
-from webob import Request
53  
-from webob.exc import HTTPBadRequest
  52
+from swift.common.swob import Request, HTTPBadRequest
54 53
 
55 54
 
56 55
 class DomainRemapMiddleware(object):
2  swift/common/middleware/healthcheck.py
@@ -13,7 +13,7 @@
13 13
 # See the License for the specific language governing permissions and
14 14
 # limitations under the License.
15 15
 
16  
-from webob import Request, Response
  16
+from swift.common.swob import Request, Response
17 17
 
18 18
 
19 19
 class HealthCheckMiddleware(object):
11  swift/common/middleware/keystoneauth.py
@@ -14,10 +14,9 @@
14 14
 # License for the specific language governing permissions and limitations
15 15
 # under the License.
16 16
 
17  
-import webob
18  
-
19 17
 from swift.common import utils as swift_utils
20 18
 from swift.common.middleware import acl as swift_acl
  19
+from swift.common.swob import HTTPNotFound, HTTPForbidden, HTTPUnauthorized
21 20
 
22 21
 
23 22
 class KeystoneAuth(object):
@@ -153,7 +152,7 @@ def authorize(self, req):
153 152
             part = swift_utils.split_path(req.path, 1, 4, True)
154 153
             version, account, container, obj = part
155 154
         except ValueError:
156  
-            return webob.exc.HTTPNotFound(request=req)
  155
+            return HTTPNotFound(request=req)
157 156
 
158 157
         user_roles = env_identity.get('roles', [])
159 158
 
@@ -226,7 +225,7 @@ def authorize_anonymous(self, req):
226 225
             part = swift_utils.split_path(req.path, 1, 4, True)
227 226
             version, account, container, obj = part
228 227
         except ValueError:
229  
-            return webob.exc.HTTPNotFound(request=req)
  228
+            return HTTPNotFound(request=req)
230 229
 
231 230
         is_authoritative_authz = (account and
232 231
                                   account.startswith(self.reseller_prefix))
@@ -274,9 +273,9 @@ def denied_response(self, req):
274 273
         depending on whether the REMOTE_USER is set or not.
275 274
         """
276 275
         if req.remote_user:
277  
-            return webob.exc.HTTPForbidden(request=req)
  276
+            return HTTPForbidden(request=req)
278 277
         else:
279  
-            return webob.exc.HTTPUnauthorized(request=req)
  278
+            return HTTPUnauthorized(request=req)
280 279
 
281 280
 
282 281
 def filter_factory(global_conf, **local_conf):
5  swift/common/middleware/name_check.py
@@ -38,10 +38,11 @@
38 38
 
39 39
 import re
40 40
 from swift.common.utils import get_logger
41  
-from webob import Request
42  
-from webob.exc import HTTPBadRequest
43 41
 from urllib2 import unquote
44 42
 
  43
+from swift.common.swob import Request, HTTPBadRequest
  44
+
  45
+
45 46
 FORBIDDEN_CHARS = "\'\"`<>"
46 47
 MAX_LENGTH = 255
47 48
 FORBIDDEN_REGEXP = "/\./|/\.\./|/\.$|/\.\.$"
3  swift/common/middleware/proxy_logging.py
@@ -40,8 +40,7 @@
40 40
 import time
41 41
 from urllib import quote, unquote
42 42
 
43  
-from webob import Request
44  
-
  43
+from swift.common.swob import Request
45 44
 from swift.common.utils import (get_logger, get_remote_client,
46 45
                                 get_valid_utf8_str, TRUE_VALUES)
47 46
 
4  swift/common/middleware/ratelimit.py
@@ -13,11 +13,11 @@
13 13
 # limitations under the License.
14 14
 import time
15 15
 import eventlet
16  
-from webob import Request, Response
17 16
 
18 17
 from swift.common.utils import split_path, cache_from_env, get_logger
19 18
 from swift.proxy.controllers.base import get_container_memcache_key
20 19
 from swift.common.memcached import MemcacheConnectionError
  20
+from swift.common.swob import Request, Response
21 21
 
22 22
 
23 23
 class MaxSleepTimeHitError(Exception):
@@ -205,7 +205,7 @@ def handle_ratelimit(self, req, account_name, container_name, obj_name):
205 205
     def __call__(self, env, start_response):
206 206
         """
207 207
         WSGI entry point.
208  
-        Wraps env in webob.Request object and passes it down.
  208
+        Wraps env in swob.Request object and passes it down.
209 209
 
210 210
         :param env: WSGI environment dictionary
211 211
         :param start_response: WSGI callable
2  swift/common/middleware/recon.py
@@ -16,7 +16,7 @@
16 16
 import errno
17 17
 import os
18 18
 
19  
-from webob import Request, Response
  19
+from swift.common.swob import Request, Response
20 20
 from swift.common.utils import split_path, get_logger, TRUE_VALUES
21 21
 from swift.common.constraints import check_mount
22 22
 from resource import getpagesize
3  swift/common/middleware/staticweb.py
@@ -118,14 +118,13 @@
118 118
 import time
119 119
 from urllib import unquote, quote as urllib_quote
120 120
 
121  
-from webob import Response
122  
-from webob.exc import HTTPMovedPermanently, HTTPNotFound
123 121
 
124 122
 from swift.common.utils import cache_from_env, get_logger, human_readable, \
125 123
                                split_path, TRUE_VALUES
126 124
 from swift.common.wsgi import make_pre_authed_env, make_pre_authed_request, \
127 125
                               WSGIContext
128 126
 from swift.common.http import is_success, is_redirection, HTTP_NOT_FOUND
  127
+from swift.common.swob import Response, HTTPMovedPermanently, HTTPNotFound
129 128
 
130 129
 
131 130
 def quote(value, safe='/'):
14  swift/common/middleware/tempauth.py
@@ -22,8 +22,8 @@
22 22
 import base64
23 23
 
24 24
 from eventlet import Timeout
25  
-from webob import Response, Request
26  
-from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPNotFound, \
  25
+from swift.common.swob import Response, Request
  26
+from swift.common.swob import HTTPBadRequest, HTTPForbidden, HTTPNotFound, \
27 27
     HTTPUnauthorized
28 28
 
29 29
 from swift.common.middleware.acl import clean_acl, parse_acl, referrer_allowed
@@ -285,7 +285,7 @@ def handle(self, env, start_response):
285 285
         """
286 286
         WSGI entry point for auth requests (ones that match the
287 287
         self.auth_prefix).
288  
-        Wraps env in webob.Request object and passes it down.
  288
+        Wraps env in swob.Request object and passes it down.
289 289
 
290 290
         :param env: WSGI environment dictionary
291 291
         :param start_response: WSGI callable
@@ -321,9 +321,9 @@ def handle(self, env, start_response):
321 321
     def handle_request(self, req):
322 322
         """
323 323
         Entry point for auth requests (ones that match the self.auth_prefix).
324  
-        Should return a WSGI-style callable (such as webob.Response).
  324
+        Should return a WSGI-style callable (such as swob.Response).
325 325
 
326  
-        :param req: webob.Request object
  326
+        :param req: swob.Request object
327 327
         """
328 328
         req.start_time = time()
329 329
         handler = None
@@ -363,8 +363,8 @@ def handle_get_token(self, req):
363 363
         X-Storage-Token set to the token to use with Swift and X-Storage-URL
364 364
         set to the URL to the default Swift cluster to use.
365 365
 
366  
-        :param req: The webob.Request to process.
367  
-        :returns: webob.Response, 2xx on success with data set as explained
  366
+        :param req: The swob.Request to process.
  367
+        :returns: swob.Response, 2xx on success with data set as explained
368 368
                   above.
369 369
         """
370 370
         # Validate the request info
840  swift/common/swob.py
... ...
@@ -0,0 +1,840 @@
  1
+# Copyright (c) 2010-2012 OpenStack, LLC.
  2
+#
  3
+# Licensed under the Apache License, Version 2.0 (the "License");
  4
+# you may not use this file except in compliance with the License.
  5
+# You may obtain a copy of the License at
  6
+#
  7
+#    http://www.apache.org/licenses/LICENSE-2.0
  8
+#
  9
+# Unless required by applicable law or agreed to in writing, software
  10
+# distributed under the License is distributed on an "AS IS" BASIS,
  11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
  12
+# implied.
  13
+# See the License for the specific language governing permissions and
  14
+# limitations under the License.
  15
+
  16
+"""
  17
+Implementation of WSGI Request and Response objects.
  18
+
  19
+This library has a very similar API to Webob.  It wraps WSGI request
  20
+environments and response values into objects that are more friendly to
  21
+interact with.
  22
+"""
  23
+
  24
+from cStringIO import StringIO
  25
+import UserDict
  26
+import time
  27
+from functools import partial
  28
+from datetime import datetime, date, timedelta, tzinfo
  29
+from email.utils import parsedate
  30
+import urlparse
  31
+import urllib2
  32
+import re
  33
+
  34
+from swift.common.utils import reiterate
  35
+
  36
+
  37
+RESPONSE_REASONS = {
  38
+    100: ('Continue', ''),
  39
+    200: ('OK', ''),
  40
+    201: ('Created', ''),
  41
+    202: ('Accepted', 'The request is accepted for processing.'),
  42
+    204: ('No Content', ''),
  43
+    206: ('Partial Content', ''),
  44
+    301: ('Moved Permanently', 'The resource has moved permanently.'),
  45
+    302: ('Found', ''),
  46
+    304: ('Not Modified', ''),
  47
+    307: ('Temporary Redirect', 'The resource has moved temporarily.'),
  48
+    400: ('Bad Request', 'The server could not comply with the request since '
  49
+          'it is either malformed or otherwise incorrect.'),
  50
+    401: ('Unauthorized', 'This server could not verify that you are '
  51
+          'authorized to access the document you requested.'),
  52
+    402: ('Payment Required', 'Access was denied for financial reasons.'),
  53
+    403: ('Forbidden', 'Access was denied to this resource.'),
  54
+    404: ('Not Found', 'The resource could not be found.'),
  55
+    405: ('Method Not Allowed', 'The method is not allowed for this '
  56
+          'resource.'),
  57
+    406: ('Not Acceptable', 'The resource is not available in a format '
  58
+          'acceptable to your browser.'),
  59
+    408: ('Request Timeout', 'The server has waited too long for the request '
  60
+          'to be sent by the client.'),
  61
+    409: ('Conflict', 'There was a conflict when trying to complete '
  62
+          'your request.'),
  63
+    410: ('Gone', 'This resource is no longer available.'),
  64
+    411: ('Length Required', 'Content-Length header required.'),
  65
+    412: ('Precondition Failed', 'A precondition for this request was not '
  66
+          'met.'),
  67
+    413: ('Request Entity Too Large', 'The body of your request was too '
  68
+          'large for this server.'),
  69
+    414: ('Request URI Too Long', 'The request URI was too long for this '
  70
+          'server.'),
  71
+    415: ('Unsupported Media Type', 'The request media type is not '
  72
+          'supported by this server.'),
  73
+    416: ('Request Range Not Satisfiable', 'The Range requested is not '
  74
+          'available.'),
  75
+    417: ('Expectation Failed', 'Expectation failed.'),
  76
+    422: ('Unprocessable Entity', 'Unable to process the contained '
  77
+          'instructions'),
  78
+    499: ('Client Disconnect', 'The client was disconnected during request.'),
  79
+    500: ('Internal Error', 'The server has either erred or is incapable of '
  80
+          'performing the requested operation.'),
  81
+    501: ('Not Implemented', 'The requested method is not implemented by '
  82
+          'this server.'),
  83
+    502: ('Bad Gateway', 'Bad gateway.'),
  84
+    503: ('Service Unavailable', 'The server is currently unavailable. '
  85
+          'Please try again at a later time.'),
  86
+    504: ('Gateway Timeout', 'A timeout has occurred speaking to a '
  87
+          'backend server.'),
  88
+    507: ('Insufficient Storage', 'There was not enough space to save the '
  89
+          'resource.'),
  90
+}
  91
+
  92
+
  93
+class _UTC(tzinfo):
  94
+    """
  95
+    A tzinfo class for datetime objects that returns a 0 timedelta (UTC time)
  96
+    """
  97
+    def dst(self, dt):
  98
+        return timedelta(0)
  99
+    utcoffset = dst
  100
+
  101
+    def tzname(self, dt):
  102
+        return 'UTC'
  103
+UTC = _UTC()
  104
+
  105
+
  106
+def _datetime_property(header):
  107
+    """
  108
+    Set and retrieve the datetime value of self.headers[header]
  109
+    (Used by both request and response)
  110
+    The header is parsed on retrieval and a datetime object is returned.
  111
+    The header can be set using a datetime, numeric value, or str.
  112
+    If a value of None is given, the header is deleted.
  113
+
  114
+    :param header: name of the header, e.g. "Content-Length"
  115
+    """
  116
+    def getter(self):
  117
+        value = self.headers.get(header, None)
  118
+        if value is not None:
  119
+            try:
  120
+                parts = parsedate(self.headers[header])[:7]
  121
+                date = datetime(*(parts + (UTC,)))
  122
+            except Exception:
  123
+                return None
  124
+            if date.year < 1970:
  125
+                raise ValueError('Somehow an invalid year')
  126
+            return date
  127
+
  128
+    def setter(self, value):
  129
+        if isinstance(value, (float, int, long)):
  130
+            self.headers[header] = time.strftime(
  131
+                "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(value))
  132
+        elif isinstance(value, datetime):
  133
+            self.headers[header] = value.strftime("%a, %d %b %Y %H:%M:%S GMT")
  134
+        else:
  135
+            self.headers[header] = value
  136
+
  137
+    return property(getter, setter,
  138
+                    doc=("Retrieve and set the %s header as a datetime, "
  139
+                         "set it with a datetime, int, or str") % header)
  140
+
  141
+
  142
+def _header_property(header):
  143
+    """
  144
+    Set and retrieve the value of self.headers[header]
  145
+    (Used by both request and response)
  146
+    If a value of None is given, the header is deleted.
  147
+
  148
+    :param header: name of the header, e.g. "Content-Length"
  149
+    """
  150
+    def getter(self):
  151
+        return self.headers.get(header, None)
  152
+
  153
+    def setter(self, value):
  154
+        self.headers[header] = value
  155
+
  156
+    return property(getter, setter,
  157
+                    doc="Retrieve and set the %s header" % header)
  158
+
  159
+
  160
+def _header_int_property(header):
  161
+    """
  162
+    Set and retrieve the value of self.headers[header]
  163
+    (Used by both request and response)
  164
+    On retrieval, it converts values to integers.
  165
+    If a value of None is given, the header is deleted.
  166
+
  167
+    :param header: name of the header, e.g. "Content-Length"
  168
+    """
  169
+    def getter(self):
  170
+        val = self.headers.get(header, None)
  171
+        if val is not None:
  172
+            val = int(val)
  173
+        return val
  174
+
  175
+    def setter(self, value):
  176
+        self.headers[header] = value
  177
+
  178
+    return property(getter, setter,
  179
+                    doc="Retrieve and set the %s header as an int" % header)
  180
+
  181
+
  182
+class HeaderEnvironProxy(UserDict.DictMixin):
  183
+    """
  184
+    A dict-like object that proxies requests to a wsgi environ,
  185
+    rewriting header keys to environ keys.
  186
+
  187
+    For example, headers['Content-Range'] sets and gets the value of
  188
+    headers.environ['HTTP_CONTENT_RANGE']
  189
+    """
  190
+    def __init__(self, environ):
  191
+        self.environ = environ
  192
+
  193
+    def _normalize(self, key):
  194
+        key = 'HTTP_' + key.replace('-', '_').upper()
  195
+        if key == 'HTTP_CONTENT_LENGTH':
  196
+            return 'CONTENT_LENGTH'
  197
+        if key == 'HTTP_CONTENT_TYPE':
  198
+            return 'CONTENT_TYPE'
  199
+        return key
  200
+
  201
+    def __getitem__(self, key):
  202
+        return self.environ[self._normalize(key)]
  203
+
  204
+    def __setitem__(self, key, value):
  205
+        if value is None:
  206
+            self.environ.pop(self._normalize(key), None)
  207
+        elif isinstance(value, unicode):
  208
+            self.environ[self._normalize(key)] = value.encode('utf-8')
  209
+        else:
  210
+            self.environ[self._normalize(key)] = str(value)
  211
+
  212
+    def __contains__(self, key):
  213
+        return self._normalize(key) in self.environ
  214
+
  215
+    def __delitem__(self, key):
  216
+        del self.environ[self._normalize(key)]
  217
+
  218
+    def keys(self):
  219
+        keys = [key[5:].replace('_', '-').title()
  220
+                for key in self.environ.iterkeys() if key.startswith('HTTP_')]
  221
+        if 'CONTENT_LENGTH' in self.environ:
  222
+            keys.append('Content-Length')
  223
+        if 'CONTENT_TYPE' in self.environ:
  224
+            keys.append('Content-Type')
  225
+        return keys
  226
+
  227
+
  228
+class HeaderKeyDict(dict):
  229
+    """
  230
+    A dict that lower-cases all keys on the way in, so as to be
  231
+    case-insensitive.
  232
+    """
  233
+    def __init__(self, *args, **kwargs):
  234
+        for arg in args:
  235
+            self.update(arg)
  236
+        self.update(kwargs)
  237
+
  238
+    def update(self, other):
  239
+        if hasattr(other, 'keys'):
  240
+            for key in other.keys():
  241
+                self[key.lower()] = other[key]
  242
+        else:
  243
+            for key, value in other:
  244
+                self[key.lower()] = value
  245
+
  246
+    def __getitem__(self, key):
  247
+        return dict.get(self, key.lower())
  248
+
  249
+    def __setitem__(self, key, value):
  250
+        if value is None:
  251
+            self.pop(key.lower(), None)
  252
+        elif isinstance(value, unicode):
  253
+            return dict.__setitem__(self, key.lower(), value.encode('utf-8'))
  254
+        else:
  255
+            return dict.__setitem__(self, key.lower(), str(value))
  256
+
  257
+    def __contains__(self, key):
  258
+        return dict.__contains__(self, key.lower())
  259
+
  260
+    def __delitem__(self, key):
  261
+        return dict.__delitem__(self, key.lower())
  262
+
  263
+    def get(self, key, default=None):
  264
+        return dict.get(self, key.lower(), default)
  265
+
  266
+
  267
+def _resp_status_property():
  268
+    """
  269
+    Set and retrieve the value of Response.status
  270
+    On retrieval, it concatenates status_int and title.
  271
+    When set to a str, it splits status_int and title apart.
  272
+    When set to an integer, retrieves the correct title for that
  273
+    response code from the RESPONSE_REASONS dict.
  274
+
  275
+    :param header: name of the header, e.g. "Content-Length"
  276
+    """
  277
+    def getter(self):
  278
+        return '%s %s' % (self.status_int, self.title)
  279
+
  280
+    def setter(self, value):
  281
+        if isinstance(value, (int, long)):
  282
+            self.status_int = value
  283
+            self.explanation = self.title = RESPONSE_REASONS[value][0]
  284
+        else:
  285
+            if isinstance(value, unicode):
  286
+                value = value.encode('utf-8')
  287
+            self.status_int = int(value.split(' ', 1)[0])
  288
+            self.explanation = self.title = value.split(' ', 1)[1]
  289
+
  290
+    return property(getter, setter,
  291
+                    doc="Retrieve and set the Response status, e.g. '200 OK'")
  292
+
  293
+
  294
+def _resp_body_property():
  295
+    """
  296
+    Set and retrieve the value of Response.body
  297
+    If necessary, it will consume Response.app_iter to create a body.
  298
+    On assignment, encodes unicode values to utf-8, and sets the content-length
  299
+    to the length of the str.
  300
+    """
  301
+    def getter(self):
  302
+        if not self._body:
  303
+            self._body = ''.join(self._app_iter)
  304
+            self._app_iter = None
  305
+        return self._body
  306
+
  307
+    def setter(self, value):
  308
+        if isinstance(value, unicode):
  309
+            value = value.encode('utf-8')
  310
+        if isinstance(value, str):
  311
+            self.content_length = len(value)
  312
+            self._app_iter = None
  313
+        self._body = value
  314
+
  315
+    return property(getter, setter,
  316
+                    doc="Retrieve and set the Response body str")
  317
+
  318
+
  319
+def _resp_etag_property():
  320
+    """
  321
+    Set and retrieve Response.etag
  322
+    This may be broken for etag use cases other than Swift's.
  323
+    Quotes strings when assigned and unquotes when read, for compatibility
  324
+    with webob.
  325
+    """
  326
+    def getter(self):
  327
+        etag = self.headers.get('etag', None)
  328
+        if etag:
  329
+            etag = etag.replace('"', '')
  330
+        return etag
  331
+
  332
+    def setter(self, value):
  333
+        if value is None:
  334
+            self.headers['etag'] = None
  335
+        else:
  336
+            self.headers['etag'] = '"%s"' % value
  337
+
  338
+    return property(getter, setter,
  339
+                    doc="Retrieve and set the response Etag header")
  340
+
  341
+
  342
+def _resp_content_type_property():
  343
+    """
  344
+    Set and retrieve Response.content_type
  345
+    Strips off any charset when retrieved -- that is accessible
  346
+    via Response.charset.
  347
+    """
  348
+    def getter(self):
  349
+        if 'content-type' in self.headers:
  350
+            return self.headers.get('content-type').split(';')[0]
  351
+
  352
+    def setter(self, value):
  353
+        self.headers['content-type'] = value
  354
+
  355
+    return property(getter, setter,
  356
+                    doc="Retrieve and set the response Content-Type header")
  357
+
  358
+
  359
+def _resp_charset_property():
  360
+    """
  361
+    Set and retrieve Response.charset
  362
+    On retrieval, separates the charset from the content-type.
  363
+    On assignment, removes any existing charset from the content-type and
  364
+    appends the new one.
  365
+    """
  366
+    def getter(self):
  367
+        if '; charset=' in self.headers['content-type']:
  368
+            return self.headers['content-type'].split('; charset=')[1]
  369
+
  370
+    def setter(self, value):
  371
+        if 'content-type' in self.headers:
  372
+            self.headers['content-type'] = self.headers['content-type'].split(
  373
+                ';')[0]
  374
+            if value:
  375
+                self.headers['content-type'] += '; charset=' + value
  376
+
  377
+    return property(getter, setter,
  378
+                    doc="Retrieve and set the response charset")
  379
+
  380
+
  381
+def _resp_app_iter_property():
  382
+    """
  383
+    Set and retrieve Response.app_iter
  384
+    Mostly a pass-through to Response._app_iter, it's a property so it can zero
  385
+    out an exsisting content-length on assignment.
  386
+    """
  387
+    def getter(self):
  388
+        return self._app_iter
  389
+
  390
+    def setter(self, value):
  391
+        if isinstance(value, (list, tuple)):
  392
+            self.content_length = sum(map(len, value))
  393
+        elif value is not None:
  394
+            self.content_length = None
  395
+            self._body = None
  396
+        self._app_iter = value
  397
+
  398
+    return property(getter, setter,
  399
+                    doc="Retrieve and set the response app_iter")
  400
+
  401
+
  402
+def _req_fancy_property(cls, header, even_if_nonexistent=False):
  403
+    """
  404
+    Set and retrieve "fancy" properties.
  405
+    On retrieval, these properties return a class that takes the value of the
  406
+    header as the only argument to their constructor.
  407
+    For assignment, those classes should implement a __str__ that converts them
  408
+    back to their header values.
  409
+
  410
+    :param header: name of the header, e.g. "Accept"
  411
+    :param even_if_nonexistent: Return a value even if the header does not
  412
+        exist.  Classes using this should be prepared to accept None as a
  413
+        parameter.
  414
+    """
  415
+    def getter(self):
  416
+        try:
  417
+            if header in self.headers or even_if_nonexistent:
  418
+                return cls(self.headers.get(header))
  419
+        except ValueError:
  420
+            return None
  421
+
  422
+    def setter(self, value):
  423
+        self.headers[header] = value
  424
+
  425
+    return property(getter, setter, doc=("Retrieve and set the %s "
  426
+                    "property in the WSGI environ, as a %s object") %
  427
+                    (header, cls.__name__))
  428
+
  429
+
  430
+class Range(object):
  431
+    """
  432
+    Wraps a Request's Range header as a friendly object.
  433
+    After initialization, "range.ranges" is populated with a list
  434
+    of (start, end) tuples denoting the requested ranges.
  435
+
  436
+    :param headerval: value of the header as a str
  437
+    """
  438
+    def __init__(self, headerval):
  439
+        headerval = headerval.replace(' ', '')
  440
+        if not headerval.lower().startswith('bytes='):
  441
+            raise ValueError('Invalid Range header: %s' % headerval)
  442
+        self.ranges = []
  443
+        for rng in headerval[6:].split(','):
  444
+            start, end = rng.split('-', 1)
  445
+            if start:
  446
+                start = int(start)
  447
+            else:
  448
+                start = None
  449
+            if end:
  450
+                end = int(end)
  451
+            else:
  452
+                end = None
  453
+            self.ranges.append((start, end))
  454
+
  455
+    def __str__(self):
  456
+        string = 'bytes='
  457
+        for start, end in self.ranges:
  458
+            if start is not None:
  459
+                string += str(start)
  460
+            string += '-'
  461
+            if end is not None:
  462
+                string += str(end)
  463
+            string += ','
  464
+        return string.rstrip(',')
  465
+
  466
+    def range_for_length(self, length):
  467
+        """
  468
+        range_for_length is used to determine the correct range of bytes to
  469
+        serve from a body, given body length argument and the Range's ranges.
  470
+
  471
+        A limitation of this method is that it can't handle multiple ranges,
  472
+        for compatibility with webob.  This should be fairly easy to extend.
  473
+
  474
+        :param length: length of the response body
  475
+        """
  476
+        if length is None or not self.ranges or len(self.ranges) != 1:
  477
+            return None
  478
+        begin, end = self.ranges[0]
  479
+        if begin is None:
  480
+            if end == 0:
  481
+                return (0, length)
  482
+            if end > length:
  483
+                return None
  484
+            return (length - end, length)
  485
+        if end is None:
  486
+            if begin == 0:
  487
+                return (0, length)
  488
+            return (begin, length)
  489
+        if begin > length:
  490
+            return None
  491
+        return (begin, min(end + 1, length))
  492
+
  493
+
  494
+class Match(object):
  495
+    """
  496
+    Wraps a Request's If-None-Match header as a friendly object.
  497
+
  498
+    :param headerval: value of the header as a str
  499
+    """
  500
+    def __init__(self, headerval):
  501
+        self.tags = set()
  502
+        for tag in headerval.split(', '):
  503
+            if tag.startswith('"') and tag.endswith('"'):
  504
+                self.tags.add(tag[1:-1])
  505
+            else:
  506
+                self.tags.add(tag)
  507
+
  508
+    def __contains__(self, val):
  509
+        return '*' in self.tags or val in self.tags
  510
+
  511
+
  512
+class Accept(object):
  513
+    """
  514
+    Wraps a Request's Accept header as a friendly object.
  515
+
  516
+    :param headerval: value of the header as a str
  517
+    """
  518
+    def __init__(self, headerval):
  519
+        self.headerval = headerval
  520
+
  521
+    def _get_types(self):
  522
+        headerval = self.headerval or '*/*'
  523
+        level = 1
  524
+        types = []
  525
+        for typ in headerval.split(','):
  526
+            quality = 1.0
  527
+            if '; q=' in typ:
  528
+                typ, quality = typ.split('; q=')
  529
+            elif ';q=' in typ:
  530
+                typ, quality = typ.split(';q=')
  531
+            quality = float(quality)
  532
+            if typ.startswith('*/'):
  533
+                quality -= 0.01
  534
+            elif typ.endswith('/*'):
  535
+                quality -= 0.01
  536
+            elif '*' in typ:
  537
+                raise AssertionError('bad accept header')
  538
+            pattern = '[a-zA-Z0-9-]+'.join([re.escape(x) for x in
  539
+                                            typ.strip().split('*')])
  540
+            types.append((quality, re.compile(pattern), typ))
  541
+        types.sort(reverse=True, key=lambda t: t[0])
  542
+        return types
  543
+
  544
+    def best_match(self, options, default_match='text/plain'):
  545
+        for quality, pattern, typ in self._get_types():
  546
+            for option in options:
  547
+                if pattern.match(option):
  548
+                    return option
  549
+        return default_match
  550
+
  551
+    def __repr__(self):
  552
+        return self.headerval
  553
+
  554
+
  555
+def _req_environ_property(environ_field):
  556
+    """
  557
+    Set and retrieve value of the environ_field entry in self.environ.
  558
+    (Used by both request and response)
  559
+    """
  560
+    def getter(self):
  561
+        return self.environ.get(environ_field, None)
  562
+
  563
+    def setter(self, value):
  564
+        self.environ[environ_field] = value
  565
+
  566
+    return property(getter, setter, doc=("Get and set the %s property "
  567
+                    "in the WSGI environment") % environ_field)
  568
+
  569
+
  570
+def _req_body_property():
  571
+    """
  572
+    Set and retrieve the Request.body parameter.  It consumes wsgi.input and
  573
+    returns the results.  On assignment, uses a StringIO to create a new
  574
+    wsgi.input.
  575
+    """
  576
+    def getter(self):
  577
+        body = self.environ['wsgi.input'].read()
  578
+        self.environ['wsgi.input'] = StringIO(body)
  579
+        return body
  580
+
  581
+    def setter(self, value):
  582
+        self.environ['wsgi.input'] = StringIO(value)
  583
+        self.environ['CONTENT_LENGTH'] = str(len(value))
  584
+
  585
+    return property(getter, setter, doc="Get and set the request body str")
  586
+
  587
+
  588
+class Request(object):
  589
+    """
  590
+    WSGI Request object.
  591
+    """
  592
+    range = _req_fancy_property(Range, 'range')
  593
+    if_none_match = _req_fancy_property(Match, 'if-none-match')
  594
+    accept = _req_fancy_property(Accept, 'http-accept', True)
  595
+    method = _req_environ_property('REQUEST_METHOD')
  596
+    referrer = referer = _req_environ_property('HTTP_REFERER')
  597
+    script_name = _req_environ_property('SCRIPT_NAME')
  598
+    path_info = _req_environ_property('PATH_INFO')
  599
+    host = _req_environ_property('HTTP_HOST')
  600
+    remote_addr = _req_environ_property('REMOTE_ADDR')
  601
+    remote_user = _req_environ_property('REMOTE_USER')
  602
+    user_agent = _req_environ_property('HTTP_USER_AGENT')
  603
+    query_string = _req_environ_property('QUERY_STRING')
  604
+    if_match = _req_environ_property('HTTP_IF_MATCH')
  605
+    body_file = _req_environ_property('wsgi.input')
  606
+    content_length = _header_int_property('content-length')
  607
+    if_modified_since = _datetime_property('if-modified-since')
  608
+    if_unmodified_since = _datetime_property('if-unmodified-since')
  609
+    body = _req_body_property()
  610
+    charset = None
  611
+    _params_cache = None
  612
+    acl = _req_environ_property('swob.ACL')
  613
+
  614
+    def __init__(self, environ):
  615
+        self.environ = environ
  616
+        self.headers = HeaderEnvironProxy(self.environ)
  617
+
  618
+    @classmethod
  619
+    def blank(cls, path, environ=None, headers=None, body=None):
  620
+        """
  621
+        Create a new request object with the given parameters, and an
  622
+        environment otherwise filled in with non-surprising default values.
  623
+        """
  624
+        headers = headers or {}
  625
+        environ = environ or {}
  626
+        if '?' in path:
  627
+            path_info, query_string = path.split('?')
  628
+        else:
  629
+            path_info = path
  630
+            query_string = ''
  631
+        env = {
  632
+            'REQUEST_METHOD': 'GET',
  633
+            'SCRIPT_NAME': '',
  634
+            'QUERY_STRING': query_string,
  635
+            'PATH_INFO': path_info,
  636
+            'SERVER_NAME': 'localhost',
  637
+            'SERVER_PORT': '80',
  638
+            'HTTP_HOST': 'localhost:80',
  639
+            'SERVER_PROTOCOL': 'HTTP/1.0',
  640
+            'wsgi.version': (1, 0),
  641
+            'wsgi.url_scheme': 'http',
  642
+            'wsgi.input': StringIO(body or ''),
  643
+            'wsgi.errors': StringIO(''),
  644
+            'wsgi.multithread': False,
  645
+            'wsgi.multiprocess': False
  646
+        }
  647
+        env.update(PATH_INFO=path_info)
  648
+        env.update(environ)
  649
+        if body is not None:
  650
+            env.update(CONTENT_LENGTH=str(len(body)))
  651
+        req = Request(env)
  652
+        for key, val in headers.iteritems():