123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384 |
- # -*- coding: utf-8 -*-
- """
- hyper/http11/connection
- ~~~~~~~~~~~~~~~~~~~~~~~
- Objects that build hyper's connection-level HTTP/1.1 abstraction.
- """
- import logging
- import os
- import socket
- import base64
- from collections import Iterable, Mapping
- import collections
- from hyperframe.frame import SettingsFrame
- from .response import HTTP11Response
- from ..tls import wrap_socket, H2C_PROTOCOL
- from ..common.bufsocket import BufferedSocket
- from ..common.exceptions import TLSUpgrade, HTTPUpgrade
- from ..common.headers import HTTPHeaderMap
- from ..common.util import to_bytestring, to_host_port_tuple
- from ..compat import bytes
- # We prefer pycohttpparser to the pure-Python interpretation
- try: # pragma: no cover
- from pycohttpparser.api import Parser
- except ImportError: # pragma: no cover
- from .parser import Parser
- log = logging.getLogger(__name__)
- BODY_CHUNKED = 1
- BODY_FLAT = 2
- class HTTP11Connection(object):
- """
- An object representing a single HTTP/1.1 connection to a server.
- :param host: The host to connect to. This may be an IP address or a
- hostname, and optionally may include a port: for example,
- ``'twitter.com'``, ``'twitter.com:443'`` or ``'127.0.0.1'``.
- :param port: (optional) The port to connect to. If not provided and one
- also isn't provided in the ``host`` parameter, defaults to 80.
- :param secure: (optional) Whether the request should use TLS. Defaults to
- ``False`` for most requests, but to ``True`` for any request issued to
- port 443.
- :param ssl_context: (optional) A class with custom certificate settings.
- If not provided then hyper's default ``SSLContext`` is used instead.
- :param proxy_host: (optional) The proxy to connect to. This can be an IP
- address or a host name and may include a port.
- :param proxy_port: (optional) The proxy port to connect to. If not provided
- and one also isn't provided in the ``proxy`` parameter,
- defaults to 8080.
- """
- def __init__(self, host, port=None, secure=None, ssl_context=None,
- proxy_host=None, proxy_port=None, **kwargs):
- if port is None:
- self.host, self.port = to_host_port_tuple(host, default_port=80)
- else:
- self.host, self.port = host, port
- # Record whether we plan to secure the request. In future this should
- # be extended to a security profile, but a bool will do for now.
- # TODO: Actually do something with this!
- if secure is not None:
- self.secure = secure
- elif self.port == 443:
- self.secure = True
- else:
- self.secure = False
- # only send http upgrade headers for non-secure connection
- self._send_http_upgrade = not self.secure
- self.ssl_context = ssl_context
- self._sock = None
- # Setup proxy details if applicable.
- if proxy_host:
- if proxy_port is None:
- self.proxy_host, self.proxy_port = to_host_port_tuple(
- proxy_host, default_port=8080
- )
- else:
- self.proxy_host, self.proxy_port = proxy_host, proxy_port
- else:
- self.proxy_host = None
- self.proxy_port = None
- #: The size of the in-memory buffer used to store data from the
- #: network. This is used as a performance optimisation. Increase buffer
- #: size to improve performance: decrease it to conserve memory.
- #: Defaults to 64kB.
- self.network_buffer_size = 65536
- #: The object used to perform HTTP/1.1 parsing. Needs to conform to
- #: the standard hyper parsing interface.
- self.parser = Parser()
- def connect(self):
- """
- Connect to the server specified when the object was created. This is a
- no-op if we're already connected.
- :returns: Nothing.
- """
- if self._sock is None:
- if not self.proxy_host:
- host = self.host
- port = self.port
- else:
- host = self.proxy_host
- port = self.proxy_port
- sock = socket.create_connection((host, port), 5)
- proto = None
- if self.secure:
- assert not self.proxy_host, "Proxy with HTTPS not supported."
- sock, proto = wrap_socket(sock, host, self.ssl_context)
- log.debug("Selected protocol: %s", proto)
- sock = BufferedSocket(sock, self.network_buffer_size)
- if proto not in ('http/1.1', None):
- raise TLSUpgrade(proto, sock)
- self._sock = sock
- return
- def request(self, method, url, body=None, headers=None):
- """
- This will send a request to the server using the HTTP request method
- ``method`` and the selector ``url``. If the ``body`` argument is
- present, it should be string or bytes object of data to send after the
- headers are finished. Strings are encoded as UTF-8. To use other
- encodings, pass a bytes object. The Content-Length header is set to the
- length of the body field.
- :param method: The request method, e.g. ``'GET'``.
- :param url: The URL to contact, e.g. ``'/path/segment'``.
- :param body: (optional) The request body to send. Must be a bytestring,
- an iterable of bytestring, or a file-like object.
- :param headers: (optional) The headers to send on the request.
- :returns: Nothing.
- """
- headers = headers or {}
- method = to_bytestring(method)
- url = to_bytestring(url)
- if not isinstance(headers, HTTPHeaderMap):
- if isinstance(headers, Mapping):
- headers = HTTPHeaderMap(headers.items())
- elif isinstance(headers, Iterable):
- headers = HTTPHeaderMap(headers)
- else:
- raise ValueError(
- 'Header argument must be a dictionary or an iterable'
- )
- if self._sock is None:
- self.connect()
- if self._send_http_upgrade:
- self._add_upgrade_headers(headers)
- self._send_http_upgrade = False
- # We may need extra headers.
- if body:
- body_type = self._add_body_headers(headers, body)
- if b'host' not in headers:
- headers[b'host'] = self.host
- # Begin by emitting the header block.
- self._send_headers(method, url, headers)
- # Next, send the request body.
- if body:
- self._send_body(body, body_type)
- return
- def get_response(self):
- """
- Returns a response object.
- This is an early beta, so the response object is pretty stupid. That's
- ok, we'll fix it later.
- """
- headers = HTTPHeaderMap()
- response = None
- while response is None:
- # 'encourage' the socket to receive data.
- self._sock.fill()
- response = self.parser.parse_response(self._sock.buffer)
- for n, v in response.headers:
- headers[n.tobytes()] = v.tobytes()
- self._sock.advance_buffer(response.consumed)
- if (response.status == 101 and
- b'upgrade' in headers['connection'] and
- H2C_PROTOCOL.encode('utf-8') in headers['upgrade']):
- raise HTTPUpgrade(H2C_PROTOCOL, self._sock)
- return HTTP11Response(
- response.status,
- response.msg.tobytes(),
- headers,
- self._sock,
- self
- )
- def _send_headers(self, method, url, headers):
- """
- Handles the logic of sending the header block.
- """
- self._sock.send(b' '.join([method, url, b'HTTP/1.1\r\n']))
- for name, value in headers.iter_raw():
- name, value = to_bytestring(name), to_bytestring(value)
- header = b''.join([name, b': ', value, b'\r\n'])
- self._sock.send(header)
- self._sock.send(b'\r\n')
- def _add_body_headers(self, headers, body):
- """
- Adds any headers needed for sending the request body. This will always
- defer to the user-supplied header content.
- :returns: One of (BODY_CHUNKED, BODY_FLAT), indicating what type of
- request body should be used.
- """
- if b'content-length' in headers:
- return BODY_FLAT
- if b'chunked' in headers.get(b'transfer-encoding', []):
- return BODY_CHUNKED
- # For bytestring bodies we upload the content with a fixed length.
- # For file objects, we use the length of the file object.
- if isinstance(body, bytes):
- length = str(len(body)).encode('utf-8')
- elif hasattr(body, 'fileno'):
- length = str(os.fstat(body.fileno()).st_size).encode('utf-8')
- else:
- length = None
- if length:
- headers[b'content-length'] = length
- return BODY_FLAT
- headers[b'transfer-encoding'] = b'chunked'
- return BODY_CHUNKED
- def _add_upgrade_headers(self, headers):
- # Add HTTP Upgrade headers.
- headers[b'connection'] = b'Upgrade, HTTP2-Settings'
- headers[b'upgrade'] = H2C_PROTOCOL
- # Encode SETTINGS frame payload in Base64 and put into the HTTP-2
- # Settings header.
- http2_settings = SettingsFrame(0)
- http2_settings.settings[SettingsFrame.INITIAL_WINDOW_SIZE] = 65535
- encoded_settings = base64.urlsafe_b64encode(
- http2_settings.serialize_body()
- )
- headers[b'HTTP2-Settings'] = encoded_settings.rstrip(b'=')
- def _send_body(self, body, body_type):
- """
- Handles the HTTP/1.1 logic for sending HTTP bodies. This does magical
- different things in different cases.
- """
- if body_type == BODY_FLAT:
- # Special case for files and other 'readable' objects.
- if hasattr(body, 'read'):
- return self._send_file_like_obj(body)
- # Case for bytestrings.
- elif isinstance(body, bytes):
- self._sock.send(body)
- return
- # Iterables that set a specific content length.
- elif isinstance(body, collections.Iterable):
- for item in body:
- try:
- self._sock.send(item)
- except TypeError:
- raise ValueError(
- "Elements in iterable body must be bytestrings. "
- "Illegal element: {}".format(item)
- )
- return
- else:
- raise ValueError(
- 'Request body must be a bytestring, a file-like object '
- 'returning bytestrings or an iterable of bytestrings. '
- 'Got: {}'.format(type(body))
- )
- # Chunked!
- return self._send_chunked(body)
- def _send_chunked(self, body):
- """
- Handles the HTTP/1.1 logic for sending a chunk-encoded body.
- """
- # Chunked! For chunked bodies we don't special-case, we just iterate
- # over what we have and send stuff out.
- for chunk in body:
- length = '{0:x}'.format(len(chunk)).encode('ascii')
- # For now write this as four 'send' calls. That's probably
- # inefficient, let's come back to it.
- try:
- self._sock.send(length)
- self._sock.send(b'\r\n')
- self._sock.send(chunk)
- self._sock.send(b'\r\n')
- except TypeError:
- raise ValueError(
- "Iterable bodies must always iterate in bytestrings"
- )
- self._sock.send(b'0\r\n\r\n')
- return
- def _send_file_like_obj(self, fobj):
- """
- Handles streaming a file-like object to the network.
- """
- while True:
- block = fobj.read(16*1024)
- if not block:
- break
- try:
- self._sock.send(block)
- except TypeError:
- raise ValueError(
- "File-like bodies must return bytestrings. Got: "
- "{}".format(type(block))
- )
- return
- def close(self):
- """
- Closes the connection. This closes the socket and then abandons the
- reference to it. After calling this method, any outstanding
- :class:`Response <hyper.http11.response.Response>` objects will throw
- exceptions if attempts are made to read their bodies.
- In some cases this method will automatically be called.
- .. warning:: This method should absolutely only be called when you are
- certain the connection object is no longer needed.
- """
- self._sock.close()
- self._sock = None
- # The following two methods are the implementation of the context manager
- # protocol.
- def __enter__(self):
- return self
- def __exit__(self, type, value, tb):
- self.close()
- return False # Never swallow exceptions.
|