|
5 | 5 |
|
6 | 6 | Objects that build hyper's connection-level HTTP/1.1 abstraction. |
7 | 7 | """ |
8 | | -import io |
9 | 8 | import logging |
10 | | -import re |
| 9 | +import os |
11 | 10 | import socket |
12 | 11 |
|
13 | 12 | from .response import HTTP11Response |
14 | 13 | from ..http20.bufsocket import BufferedSocket |
15 | 14 | from ..common.headers import HTTPHeaderMap |
16 | 15 | from ..common.util import to_bytestring |
| 16 | +from ..compat import bytes |
17 | 17 |
|
18 | 18 | log = logging.getLogger(__name__) |
19 | 19 |
|
| 20 | +BODY_CHUNKED = 1 |
| 21 | +BODY_FLAT = 2 |
| 22 | + |
20 | 23 |
|
21 | 24 | class HTTP11Connection(object): |
22 | 25 | """ |
@@ -91,24 +94,24 @@ def request(self, method, url, body=None, headers={}): |
91 | 94 | method = to_bytestring(method) |
92 | 95 | url = to_bytestring(url) |
93 | 96 |
|
| 97 | + if not isinstance(headers, HTTPHeaderMap): |
| 98 | + # FIXME: Handle things that aren't dictionaries here. |
| 99 | + headers = HTTPHeaderMap(headers.items()) |
| 100 | + |
94 | 101 | if self._sock is None: |
95 | 102 | self.connect() |
96 | 103 |
|
97 | | - # In this initial implementation, let's just write straight to the |
98 | | - # socket. We'll fix this up as we go. |
99 | | - # TODO: Fix fix fix. |
100 | | - self._sock.send(b' '.join([method, url, b'HTTP/1.1\r\n'])) |
101 | | - |
102 | | - for name, value in headers.items(): |
103 | | - name, value = to_bytestring(name), to_bytestring(value) |
104 | | - header = b''.join([name, b': ', value, b'\r\n']) |
105 | | - self._sock.send(header) |
| 104 | + # We may need extra headers. For now, we only add headers based on |
| 105 | + # body content. |
| 106 | + if body: |
| 107 | + body_type = self._add_body_headers(headers, body) |
106 | 108 |
|
107 | | - self._sock.send(b'\r\n') |
| 109 | + # Begin by emitting the header block. |
| 110 | + self._send_headers(method, url, headers) |
108 | 111 |
|
| 112 | + # Next, send the request body. |
109 | 113 | if body: |
110 | | - # TODO: Come back here to support non-string bodies. |
111 | | - self._sock.send(body) |
| 114 | + self._send_body(body, body_type) |
112 | 115 |
|
113 | 116 | return |
114 | 117 |
|
@@ -136,3 +139,107 @@ def get_response(self): |
136 | 139 | headers[name] = val |
137 | 140 |
|
138 | 141 | return HTTP11Response(code, reason, headers, self._sock) |
| 142 | + |
| 143 | + def _send_headers(self, method, url, headers): |
| 144 | + """ |
| 145 | + Handles the logic of sending the header block. |
| 146 | + """ |
| 147 | + self._sock.send(b' '.join([method, url, b'HTTP/1.1\r\n'])) |
| 148 | + |
| 149 | + for name, value in headers.iter_raw(): |
| 150 | + name, value = to_bytestring(name), to_bytestring(value) |
| 151 | + header = b''.join([name, b': ', value, b'\r\n']) |
| 152 | + self._sock.send(header) |
| 153 | + |
| 154 | + self._sock.send(b'\r\n') |
| 155 | + |
| 156 | + def _add_body_headers(self, headers, body): |
| 157 | + """ |
| 158 | + Adds any headers needed for sending the request body. This will always |
| 159 | + defer to the user-supplied header content. |
| 160 | +
|
| 161 | + :returns: One of (BODY_CHUNKED, BODY_FLAT), indicating what type of |
| 162 | + request body should be used. |
| 163 | + """ |
| 164 | + if b'content-length' in headers: |
| 165 | + return BODY_FLAT |
| 166 | + |
| 167 | + if b'chunked' in headers.get(b'transfer-encoding', []): |
| 168 | + return BODY_CHUNKED |
| 169 | + |
| 170 | + # For bytestring bodies we upload the content with a fixed length. |
| 171 | + # For file objects, we use the length of the file object. |
| 172 | + if isinstance(body, bytes): |
| 173 | + length = str(len(body)).encode('utf-8') |
| 174 | + elif hasattr(body, 'fileno'): |
| 175 | + length = str(os.fstat(body.fileno()).st_size).encode('utf-8') |
| 176 | + else: |
| 177 | + length = None |
| 178 | + |
| 179 | + if length: |
| 180 | + headers[b'content-length'] = length |
| 181 | + return BODY_FLAT |
| 182 | + |
| 183 | + headers[b'transfer-encoding'] = b'chunked' |
| 184 | + return BODY_CHUNKED |
| 185 | + |
| 186 | + def _send_body(self, body, body_type): |
| 187 | + """ |
| 188 | + Handles the HTTP/1.1 logic for sending HTTP bodies. This does magical |
| 189 | + different things in different cases. |
| 190 | + """ |
| 191 | + if body_type == BODY_FLAT: |
| 192 | + # Special case for files and other 'readable' objects. |
| 193 | + if hasattr(body, 'read'): |
| 194 | + while True: |
| 195 | + block = body.read(16*1024) |
| 196 | + if not block: |
| 197 | + break |
| 198 | + |
| 199 | + try: |
| 200 | + self._sock.send(block) |
| 201 | + except TypeError: |
| 202 | + raise ValueError( |
| 203 | + "File objects must return bytestrings" |
| 204 | + ) |
| 205 | + |
| 206 | + return |
| 207 | + |
| 208 | + # Case for bytestrings. |
| 209 | + elif isinstance(body, bytes): |
| 210 | + try: |
| 211 | + self._sock.send(body) |
| 212 | + except TypeError: |
| 213 | + raise ValueError("Body must be a bytestring") |
| 214 | + |
| 215 | + return |
| 216 | + |
| 217 | + # Iterables that set a specific content length. |
| 218 | + else: |
| 219 | + for item in body: |
| 220 | + try: |
| 221 | + self._sock.send(item) |
| 222 | + except TypeError: |
| 223 | + raise ValueError("Body must be a bytestring") |
| 224 | + |
| 225 | + return |
| 226 | + |
| 227 | + # Chunked! For chunked bodies we don't special-case, we just iterate |
| 228 | + # over what we have and send stuff out. |
| 229 | + for chunk in body: |
| 230 | + length = '{0:x}'.format(len(chunk)).encode('ascii') |
| 231 | + |
| 232 | + # For now write this as four 'send' calls. That's probably |
| 233 | + # inefficient, let's come back to it. |
| 234 | + try: |
| 235 | + self._sock.send(length) |
| 236 | + self._sock.send(b'\r\n') |
| 237 | + self._sock.send(chunk) |
| 238 | + self._sock.send(b'\r\n') |
| 239 | + except TypeError: |
| 240 | + raise ValueError( |
| 241 | + "Iterable bodies must always iterate in bytestrings" |
| 242 | + ) |
| 243 | + |
| 244 | + self._sock.send(b'0\r\n\r\n') |
| 245 | + return |
0 commit comments