-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathchunkedhttp.py
More file actions
186 lines (151 loc) · 6.81 KB
/
chunkedhttp.py
File metadata and controls
186 lines (151 loc) · 6.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
"""HTTP with chunked transfer encoding for urllib.
.. note::
This module is included here because python-icat uses it
internally, but it is not considered to be part of the API.
Changes in this module are not considered API changes of
python-icat. It may even be removed from future versions of the
python-icat distribution without further notice.
This module provides modified versions of HTTPHandler and HTTPSHandler
from urllib. These handlers differ from the standard counterparts in
that they are able to send the data using chunked transfer encoding to
the HTTP server.
Note that although the handlers are designed as drop in replacements
for the standard counterparts, we do not intent to catch all corner
cases and be fully compatible in all situations. The implementations
here shall be just good enough for the use cases in IDSClient.
Starting with Python 3.6.0, support for chunked transfer encoding has
been added to the standard library, see `Issue 12319`_. As a result,
this module is obsolete for newer Python versions and python-icat will
use it only for older versions.
.. _Issue 12319: https://bugs.python.org/issue12319
"""
import http.client
import urllib.error
import urllib.request
# We always set the Content-Length header for these methods because some
# servers will otherwise respond with a 411
_METHODS_EXPECTING_BODY = {'PATCH', 'POST', 'PUT'}
def stringiterator(buffer):
"""Wrap a string in an iterator that yields it in one single chunk."""
if len(buffer) > 0:
yield buffer
def fileiterator(f, chunksize=8192):
"""Yield the content of a file by chunks of a given size at a time."""
while True:
chunk = f.read(chunksize)
if not chunk:
break
yield chunk
class HTTPConnectionMixin:
"""Implement chunked transfer encoding in HTTP.
This is designed as a mixin class to modify either HTTPConnection
or HTTPSConnection accordingly.
"""
def _send_request(self, method, url, body, headers):
# This method is taken and modified from the Python 2.7
# httplib.py to prevent it from trying to set a Content-length
# header and to hook in our send_body() method.
# Admitted, it's an evil hack.
header_names = {k.lower(): k for k in headers.keys()}
skips = {}
if 'host' in header_names:
skips['skip_host'] = 1
if 'accept-encoding' in header_names:
skips['skip_accept_encoding'] = 1
self.putrequest(method, url, **skips)
chunked = False
if 'transfer-encoding' in header_names:
if headers[header_names['transfer-encoding']] == 'chunked':
chunked = True
else:
raise http.client.HTTPException("Invalid Transfer-Encoding")
for hdr, value in headers.items():
self.putheader(hdr, value)
self.endheaders()
self.send_body(body, chunked)
def send_body(self, body, chunked):
"""Send the body, either as is or chunked.
The empty line separating the headers from the body must have
been sent before calling this method.
"""
if body is not None:
if isinstance(body, bytes):
bodyiter = stringiterator(body)
elif isinstance(body, str):
bodyiter = stringiterator(body.encode('ascii'))
elif hasattr(body, 'read'):
bodyiter = fileiterator(body)
elif hasattr(body, '__iter__'):
bodyiter = body
else:
raise TypeError("expect either a string, a file, "
"or an iterable")
if chunked:
for chunk in bodyiter:
self.send(hex(len(chunk))[2:].encode('ascii')
+ b"\r\n" + chunk + b"\r\n")
self.send(b"0\r\n\r\n")
else:
for chunk in bodyiter:
self.send(chunk)
class HTTPConnection(HTTPConnectionMixin, http.client.HTTPConnection):
pass
class HTTPSConnection(HTTPConnectionMixin, http.client.HTTPSConnection):
pass
class HTTPHandlerMixin:
"""Internal helper class.
This is designed as a mixin class to modify either HTTPHandler or
HTTPSHandler accordingly. It overrides do_request_() inherited
from AbstractHTTPHandler.
"""
def do_request_(self, request):
# The original method from AbstractHTTPHandler sets some
# defaults that are unsuitable for our use case. In
# particular it tries to enforce Content-length to be set (and
# fails doing so if data is not a string), while for chunked
# transfer encoding Content-length must not be set.
if not request.host:
raise urllib.error.URLError('no host given')
if request.data is not None:
if not request.has_header('Content-type'):
raise urllib.error.URLError('no Content-type header given')
if not request.has_header('Content-length'):
if isinstance(request.data, (bytes, str)):
request.add_unredirected_header(
'Content-length', '%d' % len(request.data))
else:
request.add_unredirected_header(
'Transfer-Encoding', 'chunked')
else:
if request.get_method().upper() in _METHODS_EXPECTING_BODY:
request.add_unredirected_header('Content-length', '0')
sel_host = request.host
if request.has_proxy():
scheme, sel = splittype(request.selector)
sel_host, sel_path = splithost(sel)
if not request.has_header('Host'):
request.add_unredirected_header('Host', sel_host)
for name, value in self.parent.addheaders:
name = name.capitalize()
if not request.has_header(name):
request.add_unredirected_header(name, value)
return request
class HTTPHandler(HTTPHandlerMixin, urllib.request.HTTPHandler):
def http_open(self, req):
return self.do_open(HTTPConnection, req)
http_request = HTTPHandlerMixin.do_request_
class HTTPSHandler(HTTPHandlerMixin, urllib.request.HTTPSHandler):
def https_open(self, req):
if hasattr(self, '_context') and hasattr(self, '_check_hostname'):
# Python 3.2 and newer
return self.do_open(HTTPSConnection, req,
context=self._context,
check_hostname=self._check_hostname)
elif hasattr(self, '_context'):
# Python 2.7.9
return self.do_open(HTTPSConnection, req,
context=self._context)
else:
# Python 2.7.8 or 3.1 and older
return self.do_open(HTTPSConnection, req)
https_request = HTTPHandlerMixin.do_request_