httputil.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 #
00003 # Copyright 2009 Facebook
00004 #
00005 # Licensed under the Apache License, Version 2.0 (the "License"); you may
00006 # not use this file except in compliance with the License. You may obtain
00007 # a copy of the License at
00008 #
00009 #     http://www.apache.org/licenses/LICENSE-2.0
00010 #
00011 # Unless required by applicable law or agreed to in writing, software
00012 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
00013 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
00014 # License for the specific language governing permissions and limitations
00015 # under the License.
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  # py2
00040 except ImportError:
00041     import http.cookies as Cookie  # py3
00042 
00043 try:
00044     from httplib import responses  # py2
00045 except ImportError:
00046     from http.client import responses  # py3
00047 
00048 # responses is unused in this file, but we re-export it to other files.
00049 # Reference it so pyflakes doesn't complain.
00050 responses
00051 
00052 try:
00053     from urllib import urlencode  # py2
00054 except ImportError:
00055     from urllib.parse import urlencode  # py3
00056 
00057 try:
00058     from ssl import SSLError
00059 except ImportError:
00060     # ssl is unavailable on app engine.
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             # Limit the size of the cache.  LRU would be better, but this
00087             # simpler approach should be fine.  In Python 2.7+ we could
00088             # use OrderedDict (or in 3.2+, @functools.lru_cache).
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         # Don't pass args or kwargs to dict.__init__, as it will bypass
00126         # our __setitem__
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             # Copy constructor
00133             for k, v in args[0].get_all():
00134                 self.add(k, v)
00135         else:
00136             # Dict-style initialization
00137             self.update(*args, **kwargs)
00138 
00139     # new public methods
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             # bypass our override of __setitem__ since it modifies _as_list
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             # continuation of a multi-line header
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     # dict implementation overrides
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         # dict.update bypasses our __setitem__
00225         for k, v in dict(*args, **kwargs).items():
00226             self[k] = v
00227 
00228     def copy(self):
00229         # default implementation returns dict(self), not the subclass
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         # set remote IP and protocol
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     # The standard allows for the boundary to be quoted in the header,
00703     # although it's rare (it happens at least for google app engine
00704     # xmpp).  I think we're also supposed to handle backslash-escapes
00705     # here but I'll save that until we see a client that uses them
00706     # in the wild.
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 # _parseparam and _parse_header are copied and modified from python2.7's cgi.py
00804 # The original 2.7 version of this code did not correctly support some
00805 # combinations of semicolons and double quotes.
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()


rosbridge_server
Author(s): Jonathan Mace
autogenerated on Thu Jun 6 2019 21:51:50