00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017 """HTTP utility code shared by clients and servers.
00018
00019 This module also defines the `HTTPServerRequest` class which is exposed
00020 via `tornado.web.RequestHandler.request`.
00021 """
00022
00023 from __future__ import absolute_import, division, print_function, with_statement
00024
00025 import calendar
00026 import collections
00027 import copy
00028 import datetime
00029 import email.utils
00030 import numbers
00031 import re
00032 import time
00033
00034 from tornado.escape import native_str, parse_qs_bytes, utf8
00035 from tornado.log import gen_log
00036 from tornado.util import ObjectDict, bytes_type
00037
00038 try:
00039 import Cookie
00040 except ImportError:
00041 import http.cookies as Cookie
00042
00043 try:
00044 from httplib import responses
00045 except ImportError:
00046 from http.client import responses
00047
00048
00049
00050 responses
00051
00052 try:
00053 from urllib import urlencode
00054 except ImportError:
00055 from urllib.parse import urlencode
00056
00057 try:
00058 from ssl import SSLError
00059 except ImportError:
00060
00061 class SSLError(Exception):
00062 pass
00063
00064
00065 class _NormalizedHeaderCache(dict):
00066 """Dynamic cached mapping of header names to Http-Header-Case.
00067
00068 Implemented as a dict subclass so that cache hits are as fast as a
00069 normal dict lookup, without the overhead of a python function
00070 call.
00071
00072 >>> normalized_headers = _NormalizedHeaderCache(10)
00073 >>> normalized_headers["coNtent-TYPE"]
00074 'Content-Type'
00075 """
00076 def __init__(self, size):
00077 super(_NormalizedHeaderCache, self).__init__()
00078 self.size = size
00079 self.queue = collections.deque()
00080
00081 def __missing__(self, key):
00082 normalized = "-".join([w.capitalize() for w in key.split("-")])
00083 self[key] = normalized
00084 self.queue.append(key)
00085 if len(self.queue) > self.size:
00086
00087
00088
00089 old_key = self.queue.popleft()
00090 del self[old_key]
00091 return normalized
00092
00093 _normalized_headers = _NormalizedHeaderCache(1000)
00094
00095
00096 class HTTPHeaders(dict):
00097 """A dictionary that maintains ``Http-Header-Case`` for all keys.
00098
00099 Supports multiple values per key via a pair of new methods,
00100 `add()` and `get_list()`. The regular dictionary interface
00101 returns a single value per key, with multiple values joined by a
00102 comma.
00103
00104 >>> h = HTTPHeaders({"content-type": "text/html"})
00105 >>> list(h.keys())
00106 ['Content-Type']
00107 >>> h["Content-Type"]
00108 'text/html'
00109
00110 >>> h.add("Set-Cookie", "A=B")
00111 >>> h.add("Set-Cookie", "C=D")
00112 >>> h["set-cookie"]
00113 'A=B,C=D'
00114 >>> h.get_list("set-cookie")
00115 ['A=B', 'C=D']
00116
00117 >>> for (k,v) in sorted(h.get_all()):
00118 ... print('%s: %s' % (k,v))
00119 ...
00120 Content-Type: text/html
00121 Set-Cookie: A=B
00122 Set-Cookie: C=D
00123 """
00124 def __init__(self, *args, **kwargs):
00125
00126
00127 dict.__init__(self)
00128 self._as_list = {}
00129 self._last_key = None
00130 if (len(args) == 1 and len(kwargs) == 0 and
00131 isinstance(args[0], HTTPHeaders)):
00132
00133 for k, v in args[0].get_all():
00134 self.add(k, v)
00135 else:
00136
00137 self.update(*args, **kwargs)
00138
00139
00140
00141 def add(self, name, value):
00142 """Adds a new value for the given key."""
00143 norm_name = _normalized_headers[name]
00144 self._last_key = norm_name
00145 if norm_name in self:
00146
00147 dict.__setitem__(self, norm_name,
00148 native_str(self[norm_name]) + ',' +
00149 native_str(value))
00150 self._as_list[norm_name].append(value)
00151 else:
00152 self[norm_name] = value
00153
00154 def get_list(self, name):
00155 """Returns all values for the given header as a list."""
00156 norm_name = _normalized_headers[name]
00157 return self._as_list.get(norm_name, [])
00158
00159 def get_all(self):
00160 """Returns an iterable of all (name, value) pairs.
00161
00162 If a header has multiple values, multiple pairs will be
00163 returned with the same name.
00164 """
00165 for name, values in self._as_list.items():
00166 for value in values:
00167 yield (name, value)
00168
00169 def parse_line(self, line):
00170 """Updates the dictionary with a single header line.
00171
00172 >>> h = HTTPHeaders()
00173 >>> h.parse_line("Content-Type: text/html")
00174 >>> h.get('content-type')
00175 'text/html'
00176 """
00177 if line[0].isspace():
00178
00179 new_part = ' ' + line.lstrip()
00180 self._as_list[self._last_key][-1] += new_part
00181 dict.__setitem__(self, self._last_key,
00182 self[self._last_key] + new_part)
00183 else:
00184 name, value = line.split(":", 1)
00185 self.add(name, value.strip())
00186
00187 @classmethod
00188 def parse(cls, headers):
00189 """Returns a dictionary from HTTP header text.
00190
00191 >>> h = HTTPHeaders.parse("Content-Type: text/html\\r\\nContent-Length: 42\\r\\n")
00192 >>> sorted(h.items())
00193 [('Content-Length', '42'), ('Content-Type', 'text/html')]
00194 """
00195 h = cls()
00196 for line in headers.splitlines():
00197 if line:
00198 h.parse_line(line)
00199 return h
00200
00201
00202
00203 def __setitem__(self, name, value):
00204 norm_name = _normalized_headers[name]
00205 dict.__setitem__(self, norm_name, value)
00206 self._as_list[norm_name] = [value]
00207
00208 def __getitem__(self, name):
00209 return dict.__getitem__(self, _normalized_headers[name])
00210
00211 def __delitem__(self, name):
00212 norm_name = _normalized_headers[name]
00213 dict.__delitem__(self, norm_name)
00214 del self._as_list[norm_name]
00215
00216 def __contains__(self, name):
00217 norm_name = _normalized_headers[name]
00218 return dict.__contains__(self, norm_name)
00219
00220 def get(self, name, default=None):
00221 return dict.get(self, _normalized_headers[name], default)
00222
00223 def update(self, *args, **kwargs):
00224
00225 for k, v in dict(*args, **kwargs).items():
00226 self[k] = v
00227
00228 def copy(self):
00229
00230 return HTTPHeaders(self)
00231
00232
00233 class HTTPServerRequest(object):
00234 """A single HTTP request.
00235
00236 All attributes are type `str` unless otherwise noted.
00237
00238 .. attribute:: method
00239
00240 HTTP request method, e.g. "GET" or "POST"
00241
00242 .. attribute:: uri
00243
00244 The requested uri.
00245
00246 .. attribute:: path
00247
00248 The path portion of `uri`
00249
00250 .. attribute:: query
00251
00252 The query portion of `uri`
00253
00254 .. attribute:: version
00255
00256 HTTP version specified in request, e.g. "HTTP/1.1"
00257
00258 .. attribute:: headers
00259
00260 `.HTTPHeaders` dictionary-like object for request headers. Acts like
00261 a case-insensitive dictionary with additional methods for repeated
00262 headers.
00263
00264 .. attribute:: body
00265
00266 Request body, if present, as a byte string.
00267
00268 .. attribute:: remote_ip
00269
00270 Client's IP address as a string. If ``HTTPServer.xheaders`` is set,
00271 will pass along the real IP address provided by a load balancer
00272 in the ``X-Real-Ip`` or ``X-Forwarded-For`` header.
00273
00274 .. versionchanged:: 3.1
00275 The list format of ``X-Forwarded-For`` is now supported.
00276
00277 .. attribute:: protocol
00278
00279 The protocol used, either "http" or "https". If ``HTTPServer.xheaders``
00280 is set, will pass along the protocol used by a load balancer if
00281 reported via an ``X-Scheme`` header.
00282
00283 .. attribute:: host
00284
00285 The requested hostname, usually taken from the ``Host`` header.
00286
00287 .. attribute:: arguments
00288
00289 GET/POST arguments are available in the arguments property, which
00290 maps arguments names to lists of values (to support multiple values
00291 for individual names). Names are of type `str`, while arguments
00292 are byte strings. Note that this is different from
00293 `.RequestHandler.get_argument`, which returns argument values as
00294 unicode strings.
00295
00296 .. attribute:: query_arguments
00297
00298 Same format as ``arguments``, but contains only arguments extracted
00299 from the query string.
00300
00301 .. versionadded:: 3.2
00302
00303 .. attribute:: body_arguments
00304
00305 Same format as ``arguments``, but contains only arguments extracted
00306 from the request body.
00307
00308 .. versionadded:: 3.2
00309
00310 .. attribute:: files
00311
00312 File uploads are available in the files property, which maps file
00313 names to lists of `.HTTPFile`.
00314
00315 .. attribute:: connection
00316
00317 An HTTP request is attached to a single HTTP connection, which can
00318 be accessed through the "connection" attribute. Since connections
00319 are typically kept open in HTTP/1.1, multiple requests can be handled
00320 sequentially on a single connection.
00321
00322 .. versionchanged:: 4.0
00323 Moved from ``tornado.httpserver.HTTPRequest``.
00324 """
00325 def __init__(self, method=None, uri=None, version="HTTP/1.0", headers=None,
00326 body=None, host=None, files=None, connection=None,
00327 start_line=None):
00328 if start_line is not None:
00329 method, uri, version = start_line
00330 self.method = method
00331 self.uri = uri
00332 self.version = version
00333 self.headers = headers or HTTPHeaders()
00334 self.body = body or ""
00335
00336
00337 context = getattr(connection, 'context', None)
00338 self.remote_ip = getattr(context, 'remote_ip')
00339 self.protocol = getattr(context, 'protocol', "http")
00340
00341 self.host = host or self.headers.get("Host") or "127.0.0.1"
00342 self.files = files or {}
00343 self.connection = connection
00344 self._start_time = time.time()
00345 self._finish_time = None
00346
00347 self.path, sep, self.query = uri.partition('?')
00348 self.arguments = parse_qs_bytes(self.query, keep_blank_values=True)
00349 self.query_arguments = copy.deepcopy(self.arguments)
00350 self.body_arguments = {}
00351
00352 def supports_http_1_1(self):
00353 """Returns True if this request supports HTTP/1.1 semantics.
00354
00355 .. deprecated:: 4.0
00356 Applications are less likely to need this information with the
00357 introduction of `.HTTPConnection`. If you still need it, access
00358 the ``version`` attribute directly.
00359 """
00360 return self.version == "HTTP/1.1"
00361
00362 @property
00363 def cookies(self):
00364 """A dictionary of Cookie.Morsel objects."""
00365 if not hasattr(self, "_cookies"):
00366 self._cookies = Cookie.SimpleCookie()
00367 if "Cookie" in self.headers:
00368 try:
00369 self._cookies.load(
00370 native_str(self.headers["Cookie"]))
00371 except Exception:
00372 self._cookies = {}
00373 return self._cookies
00374
00375 def write(self, chunk, callback=None):
00376 """Writes the given chunk to the response stream.
00377
00378 .. deprecated:: 4.0
00379 Use ``request.connection`` and the `.HTTPConnection` methods
00380 to write the response.
00381 """
00382 assert isinstance(chunk, bytes_type)
00383 self.connection.write(chunk, callback=callback)
00384
00385 def finish(self):
00386 """Finishes this HTTP request on the open connection.
00387
00388 .. deprecated:: 4.0
00389 Use ``request.connection`` and the `.HTTPConnection` methods
00390 to write the response.
00391 """
00392 self.connection.finish()
00393 self._finish_time = time.time()
00394
00395 def full_url(self):
00396 """Reconstructs the full URL for this request."""
00397 return self.protocol + "://" + self.host + self.uri
00398
00399 def request_time(self):
00400 """Returns the amount of time it took for this request to execute."""
00401 if self._finish_time is None:
00402 return time.time() - self._start_time
00403 else:
00404 return self._finish_time - self._start_time
00405
00406 def get_ssl_certificate(self, binary_form=False):
00407 """Returns the client's SSL certificate, if any.
00408
00409 To use client certificates, the HTTPServer must have been constructed
00410 with cert_reqs set in ssl_options, e.g.::
00411
00412 server = HTTPServer(app,
00413 ssl_options=dict(
00414 certfile="foo.crt",
00415 keyfile="foo.key",
00416 cert_reqs=ssl.CERT_REQUIRED,
00417 ca_certs="cacert.crt"))
00418
00419 By default, the return value is a dictionary (or None, if no
00420 client certificate is present). If ``binary_form`` is true, a
00421 DER-encoded form of the certificate is returned instead. See
00422 SSLSocket.getpeercert() in the standard library for more
00423 details.
00424 http://docs.python.org/library/ssl.html#sslsocket-objects
00425 """
00426 try:
00427 return self.connection.stream.socket.getpeercert(
00428 binary_form=binary_form)
00429 except SSLError:
00430 return None
00431
00432 def _parse_body(self):
00433 parse_body_arguments(
00434 self.headers.get("Content-Type", ""), self.body,
00435 self.body_arguments, self.files,
00436 self.headers)
00437
00438 for k, v in self.body_arguments.items():
00439 self.arguments.setdefault(k, []).extend(v)
00440
00441 def __repr__(self):
00442 attrs = ("protocol", "host", "method", "uri", "version", "remote_ip")
00443 args = ", ".join(["%s=%r" % (n, getattr(self, n)) for n in attrs])
00444 return "%s(%s, headers=%s)" % (
00445 self.__class__.__name__, args, dict(self.headers))
00446
00447
00448 class HTTPInputError(Exception):
00449 """Exception class for malformed HTTP requests or responses
00450 from remote sources.
00451
00452 .. versionadded:: 4.0
00453 """
00454 pass
00455
00456
00457 class HTTPOutputError(Exception):
00458 """Exception class for errors in HTTP output.
00459
00460 .. versionadded:: 4.0
00461 """
00462 pass
00463
00464
00465 class HTTPServerConnectionDelegate(object):
00466 """Implement this interface to handle requests from `.HTTPServer`.
00467
00468 .. versionadded:: 4.0
00469 """
00470 def start_request(self, server_conn, request_conn):
00471 """This method is called by the server when a new request has started.
00472
00473 :arg server_conn: is an opaque object representing the long-lived
00474 (e.g. tcp-level) connection.
00475 :arg request_conn: is a `.HTTPConnection` object for a single
00476 request/response exchange.
00477
00478 This method should return a `.HTTPMessageDelegate`.
00479 """
00480 raise NotImplementedError()
00481
00482 def on_close(self, server_conn):
00483 """This method is called when a connection has been closed.
00484
00485 :arg server_conn: is a server connection that has previously been
00486 passed to ``start_request``.
00487 """
00488 pass
00489
00490
00491 class HTTPMessageDelegate(object):
00492 """Implement this interface to handle an HTTP request or response.
00493
00494 .. versionadded:: 4.0
00495 """
00496 def headers_received(self, start_line, headers):
00497 """Called when the HTTP headers have been received and parsed.
00498
00499 :arg start_line: a `.RequestStartLine` or `.ResponseStartLine`
00500 depending on whether this is a client or server message.
00501 :arg headers: a `.HTTPHeaders` instance.
00502
00503 Some `.HTTPConnection` methods can only be called during
00504 ``headers_received``.
00505
00506 May return a `.Future`; if it does the body will not be read
00507 until it is done.
00508 """
00509 pass
00510
00511 def data_received(self, chunk):
00512 """Called when a chunk of data has been received.
00513
00514 May return a `.Future` for flow control.
00515 """
00516 pass
00517
00518 def finish(self):
00519 """Called after the last chunk of data has been received."""
00520 pass
00521
00522 def on_connection_close(self):
00523 """Called if the connection is closed without finishing the request.
00524
00525 If ``headers_received`` is called, either ``finish`` or
00526 ``on_connection_close`` will be called, but not both.
00527 """
00528 pass
00529
00530
00531 class HTTPConnection(object):
00532 """Applications use this interface to write their responses.
00533
00534 .. versionadded:: 4.0
00535 """
00536 def write_headers(self, start_line, headers, chunk=None, callback=None):
00537 """Write an HTTP header block.
00538
00539 :arg start_line: a `.RequestStartLine` or `.ResponseStartLine`.
00540 :arg headers: a `.HTTPHeaders` instance.
00541 :arg chunk: the first (optional) chunk of data. This is an optimization
00542 so that small responses can be written in the same call as their
00543 headers.
00544 :arg callback: a callback to be run when the write is complete.
00545
00546 Returns a `.Future` if no callback is given.
00547 """
00548 raise NotImplementedError()
00549
00550 def write(self, chunk, callback=None):
00551 """Writes a chunk of body data.
00552
00553 The callback will be run when the write is complete. If no callback
00554 is given, returns a Future.
00555 """
00556 raise NotImplementedError()
00557
00558 def finish(self):
00559 """Indicates that the last body data has been written.
00560 """
00561 raise NotImplementedError()
00562
00563
00564 def url_concat(url, args):
00565 """Concatenate url and argument dictionary regardless of whether
00566 url has existing query parameters.
00567
00568 >>> url_concat("http://example.com/foo?a=b", dict(c="d"))
00569 'http://example.com/foo?a=b&c=d'
00570 """
00571 if not args:
00572 return url
00573 if url[-1] not in ('?', '&'):
00574 url += '&' if ('?' in url) else '?'
00575 return url + urlencode(args)
00576
00577
00578 class HTTPFile(ObjectDict):
00579 """Represents a file uploaded via a form.
00580
00581 For backwards compatibility, its instance attributes are also
00582 accessible as dictionary keys.
00583
00584 * ``filename``
00585 * ``body``
00586 * ``content_type``
00587 """
00588 pass
00589
00590
00591 def _parse_request_range(range_header):
00592 """Parses a Range header.
00593
00594 Returns either ``None`` or tuple ``(start, end)``.
00595 Note that while the HTTP headers use inclusive byte positions,
00596 this method returns indexes suitable for use in slices.
00597
00598 >>> start, end = _parse_request_range("bytes=1-2")
00599 >>> start, end
00600 (1, 3)
00601 >>> [0, 1, 2, 3, 4][start:end]
00602 [1, 2]
00603 >>> _parse_request_range("bytes=6-")
00604 (6, None)
00605 >>> _parse_request_range("bytes=-6")
00606 (-6, None)
00607 >>> _parse_request_range("bytes=-0")
00608 (None, 0)
00609 >>> _parse_request_range("bytes=")
00610 (None, None)
00611 >>> _parse_request_range("foo=42")
00612 >>> _parse_request_range("bytes=1-2,6-10")
00613
00614 Note: only supports one range (ex, ``bytes=1-2,6-10`` is not allowed).
00615
00616 See [0] for the details of the range header.
00617
00618 [0]: http://greenbytes.de/tech/webdav/draft-ietf-httpbis-p5-range-latest.html#byte.ranges
00619 """
00620 unit, _, value = range_header.partition("=")
00621 unit, value = unit.strip(), value.strip()
00622 if unit != "bytes":
00623 return None
00624 start_b, _, end_b = value.partition("-")
00625 try:
00626 start = _int_or_none(start_b)
00627 end = _int_or_none(end_b)
00628 except ValueError:
00629 return None
00630 if end is not None:
00631 if start is None:
00632 if end != 0:
00633 start = -end
00634 end = None
00635 else:
00636 end += 1
00637 return (start, end)
00638
00639
00640 def _get_content_range(start, end, total):
00641 """Returns a suitable Content-Range header:
00642
00643 >>> print(_get_content_range(None, 1, 4))
00644 bytes 0-0/4
00645 >>> print(_get_content_range(1, 3, 4))
00646 bytes 1-2/4
00647 >>> print(_get_content_range(None, None, 4))
00648 bytes 0-3/4
00649 """
00650 start = start or 0
00651 end = (end or total) - 1
00652 return "bytes %s-%s/%s" % (start, end, total)
00653
00654
00655 def _int_or_none(val):
00656 val = val.strip()
00657 if val == "":
00658 return None
00659 return int(val)
00660
00661
00662 def parse_body_arguments(content_type, body, arguments, files, headers=None):
00663 """Parses a form request body.
00664
00665 Supports ``application/x-www-form-urlencoded`` and
00666 ``multipart/form-data``. The ``content_type`` parameter should be
00667 a string and ``body`` should be a byte string. The ``arguments``
00668 and ``files`` parameters are dictionaries that will be updated
00669 with the parsed contents.
00670 """
00671 if headers and 'Content-Encoding' in headers:
00672 gen_log.warning("Unsupported Content-Encoding: %s",
00673 headers['Content-Encoding'])
00674 return
00675 if content_type.startswith("application/x-www-form-urlencoded"):
00676 try:
00677 uri_arguments = parse_qs_bytes(native_str(body), keep_blank_values=True)
00678 except Exception as e:
00679 gen_log.warning('Invalid x-www-form-urlencoded body: %s', e)
00680 uri_arguments = {}
00681 for name, values in uri_arguments.items():
00682 if values:
00683 arguments.setdefault(name, []).extend(values)
00684 elif content_type.startswith("multipart/form-data"):
00685 fields = content_type.split(";")
00686 for field in fields:
00687 k, sep, v = field.strip().partition("=")
00688 if k == "boundary" and v:
00689 parse_multipart_form_data(utf8(v), body, arguments, files)
00690 break
00691 else:
00692 gen_log.warning("Invalid multipart/form-data")
00693
00694
00695 def parse_multipart_form_data(boundary, data, arguments, files):
00696 """Parses a ``multipart/form-data`` body.
00697
00698 The ``boundary`` and ``data`` parameters are both byte strings.
00699 The dictionaries given in the arguments and files parameters
00700 will be updated with the contents of the body.
00701 """
00702
00703
00704
00705
00706
00707 if boundary.startswith(b'"') and boundary.endswith(b'"'):
00708 boundary = boundary[1:-1]
00709 final_boundary_index = data.rfind(b"--" + boundary + b"--")
00710 if final_boundary_index == -1:
00711 gen_log.warning("Invalid multipart/form-data: no final boundary")
00712 return
00713 parts = data[:final_boundary_index].split(b"--" + boundary + b"\r\n")
00714 for part in parts:
00715 if not part:
00716 continue
00717 eoh = part.find(b"\r\n\r\n")
00718 if eoh == -1:
00719 gen_log.warning("multipart/form-data missing headers")
00720 continue
00721 headers = HTTPHeaders.parse(part[:eoh].decode("utf-8"))
00722 disp_header = headers.get("Content-Disposition", "")
00723 disposition, disp_params = _parse_header(disp_header)
00724 if disposition != "form-data" or not part.endswith(b"\r\n"):
00725 gen_log.warning("Invalid multipart/form-data")
00726 continue
00727 value = part[eoh + 4:-2]
00728 if not disp_params.get("name"):
00729 gen_log.warning("multipart/form-data value missing name")
00730 continue
00731 name = disp_params["name"]
00732 if disp_params.get("filename"):
00733 ctype = headers.get("Content-Type", "application/unknown")
00734 files.setdefault(name, []).append(HTTPFile(
00735 filename=disp_params["filename"], body=value,
00736 content_type=ctype))
00737 else:
00738 arguments.setdefault(name, []).append(value)
00739
00740
00741 def format_timestamp(ts):
00742 """Formats a timestamp in the format used by HTTP.
00743
00744 The argument may be a numeric timestamp as returned by `time.time`,
00745 a time tuple as returned by `time.gmtime`, or a `datetime.datetime`
00746 object.
00747
00748 >>> format_timestamp(1359312200)
00749 'Sun, 27 Jan 2013 18:43:20 GMT'
00750 """
00751 if isinstance(ts, numbers.Real):
00752 pass
00753 elif isinstance(ts, (tuple, time.struct_time)):
00754 ts = calendar.timegm(ts)
00755 elif isinstance(ts, datetime.datetime):
00756 ts = calendar.timegm(ts.utctimetuple())
00757 else:
00758 raise TypeError("unknown timestamp type: %r" % ts)
00759 return email.utils.formatdate(ts, usegmt=True)
00760
00761
00762 RequestStartLine = collections.namedtuple(
00763 'RequestStartLine', ['method', 'path', 'version'])
00764
00765
00766 def parse_request_start_line(line):
00767 """Returns a (method, path, version) tuple for an HTTP 1.x request line.
00768
00769 The response is a `collections.namedtuple`.
00770
00771 >>> parse_request_start_line("GET /foo HTTP/1.1")
00772 RequestStartLine(method='GET', path='/foo', version='HTTP/1.1')
00773 """
00774 try:
00775 method, path, version = line.split(" ")
00776 except ValueError:
00777 raise HTTPInputError("Malformed HTTP request line")
00778 if not version.startswith("HTTP/"):
00779 raise HTTPInputError(
00780 "Malformed HTTP version in HTTP Request-Line: %r" % version)
00781 return RequestStartLine(method, path, version)
00782
00783
00784 ResponseStartLine = collections.namedtuple(
00785 'ResponseStartLine', ['version', 'code', 'reason'])
00786
00787
00788 def parse_response_start_line(line):
00789 """Returns a (version, code, reason) tuple for an HTTP 1.x response line.
00790
00791 The response is a `collections.namedtuple`.
00792
00793 >>> parse_response_start_line("HTTP/1.1 200 OK")
00794 ResponseStartLine(version='HTTP/1.1', code=200, reason='OK')
00795 """
00796 line = native_str(line)
00797 match = re.match("(HTTP/1.[01]) ([0-9]+) ([^\r]*)", line)
00798 if not match:
00799 raise HTTPInputError("Error parsing response start line")
00800 return ResponseStartLine(match.group(1), int(match.group(2)),
00801 match.group(3))
00802
00803
00804
00805
00806
00807
00808 def _parseparam(s):
00809 while s[:1] == ';':
00810 s = s[1:]
00811 end = s.find(';')
00812 while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2:
00813 end = s.find(';', end + 1)
00814 if end < 0:
00815 end = len(s)
00816 f = s[:end]
00817 yield f.strip()
00818 s = s[end:]
00819
00820
00821 def _parse_header(line):
00822 """Parse a Content-type like header.
00823
00824 Return the main content-type and a dictionary of options.
00825
00826 """
00827 parts = _parseparam(';' + line)
00828 key = next(parts)
00829 pdict = {}
00830 for p in parts:
00831 i = p.find('=')
00832 if i >= 0:
00833 name = p[:i].strip().lower()
00834 value = p[i + 1:].strip()
00835 if len(value) >= 2 and value[0] == value[-1] == '"':
00836 value = value[1:-1]
00837 value = value.replace('\\\\', '\\').replace('\\"', '"')
00838 pdict[name] = value
00839 return key, pdict
00840
00841
00842 def doctests():
00843 import doctest
00844 return doctest.DocTestSuite()