summaryrefslogtreecommitdiff
path: root/src/M2Crypto/httpslib.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/M2Crypto/httpslib.py')
-rw-r--r--src/M2Crypto/httpslib.py268
1 files changed, 268 insertions, 0 deletions
diff --git a/src/M2Crypto/httpslib.py b/src/M2Crypto/httpslib.py
new file mode 100644
index 0000000..0deee64
--- /dev/null
+++ b/src/M2Crypto/httpslib.py
@@ -0,0 +1,268 @@
+from __future__ import absolute_import
+
+import warnings
+
+"""M2Crypto support for Python's httplib.
+
+Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved."""
+
+import base64
+import socket
+
+from M2Crypto import SSL, six
+from M2Crypto.six.moves.urllib_parse import urlsplit, urlunsplit
+from M2Crypto.six.moves.http_client import * # noqa
+# This is not imported with just '*'
+from M2Crypto.six.moves.http_client import HTTPS_PORT
+from typing import Any, AnyStr, Callable, Dict, Optional # noqa
+
+
+class HTTPSConnection(HTTPConnection):
+
+ """
+ This class allows communication via SSL using M2Crypto.
+ """
+
+ default_port = HTTPS_PORT
+
+ def __init__(self, host, port=None, strict=None, **ssl):
+ # type: (str, Optional[int], Optional[bool], **Any) -> None
+ """
+ Represents one transaction with an HTTP server over the SSL
+ connection.
+
+ :param host: host name
+ :param port: port number
+ :param strict: if switched on, it raises BadStatusLine to be
+ raised if the status line can't be parsed as
+ a valid HTTP/1.0 or 1.1 status line.
+ :param ssl: dict with all remaining named real parameters of the
+ function. Specifically, ``ssl_context`` is expected
+ to be included with SSL.Context; if it is not
+ default ``'sslv23'`` is substituted).
+ """
+ self.session = None # type: bytes
+ self.host = host
+ self.port = port
+ keys = set(ssl.keys()) - set(('key_file', 'cert_file', 'ssl_context'))
+ if keys:
+ raise ValueError('unknown keyword argument: %s', keys)
+ try:
+ self.ssl_ctx = ssl['ssl_context']
+ assert isinstance(self.ssl_ctx, SSL.Context), self.ssl_ctx
+ except KeyError:
+ self.ssl_ctx = SSL.Context()
+ HTTPConnection.__init__(self, host, port, strict)
+
+ def connect(self):
+ # type: () -> None
+ error = None
+ # We ignore the returned sockaddr because SSL.Connection.connect needs
+ # a host name.
+ for (family, _, _, _, _) in \
+ socket.getaddrinfo(self.host, self.port, 0,
+ socket.SOCK_STREAM):
+ sock = None
+ try:
+ sock = SSL.Connection(self.ssl_ctx, family=family)
+
+ # set SNI server name since we know it at this point
+ sock.set_tlsext_host_name(self.host)
+
+ if self.session is not None:
+ sock.set_session(self.session)
+ sock.connect((self.host, self.port))
+
+ self.sock = sock
+ sock = None
+ return
+ except socket.error as e:
+ # Other exception are probably SSL-related, in that case we
+ # abort and the exception is forwarded to the caller.
+ error = e
+ finally:
+ if sock is not None:
+ sock.close()
+
+ if error is None:
+ raise AssertionError("Empty list returned by getaddrinfo")
+ raise error
+
+ def close(self):
+ # type: () -> None
+ # This kludges around line 545 of httplib.py,
+ # which closes the connection in this object;
+ # the connection remains open in the response
+ # object.
+ #
+ # M2Crypto doesn't close-here-keep-open-there,
+ # so, in effect, we don't close until the whole
+ # business is over and gc kicks in.
+ #
+ # XXX Long-running callers beware leakage.
+ #
+ # XXX 05-Jan-2002: This module works with Python 2.2,
+ # XXX but I've not investigated if the above conditions
+ # XXX remain.
+ pass
+
+ def get_session(self):
+ # type: () -> SSL.Session.Session
+ return self.sock.get_session()
+
+ def set_session(self, session):
+ # type: (SSL.Session.Session) -> None
+ self.session = session
+
+
+class ProxyHTTPSConnection(HTTPSConnection):
+ """
+ An HTTPS Connection that uses a proxy and the CONNECT request.
+
+ When the connection is initiated, CONNECT is first sent to the proxy (along
+ with authorization headers, if supplied). If successful, an SSL connection
+ will be established over the socket through the proxy and to the target
+ host.
+
+ Finally, the actual request is sent over the SSL connection tunneling
+ through the proxy.
+ """
+
+ _ports = {'http': 80, 'https': 443}
+ _AUTH_HEADER = "Proxy-Authorization"
+ _UA_HEADER = "User-Agent"
+
+ def __init__(self, host, port=None, strict=None, username=None,
+ password=None, **ssl):
+ # type: (str, Optional[int], Optional[bool], Optional[AnyStr], Optional[AnyStr], **Any) -> None
+ """
+ Create the ProxyHTTPSConnection object.
+
+ :param host: host name of the proxy server
+ :param port: port number of the proxy server
+ :param strict: if switched on, it raises BadStatusLine to be
+ raised if the status line can't be parsed as
+ a valid HTTP/1.0 or 1.1 status line.
+ :param username: username on the proxy server, when required
+ Username can be ``str``, but preferred type
+ is ``bytes``. M2Crypto does some conversion to
+ ``bytes`` when necessary, but it's better when
+ the user of the library does it on its own.
+ :param password: password on the proxy server, when required
+ The same as with ``username``, ``str`` is accepted,
+ but ``bytes`` are preferred.
+ :param ssl: dict with all remaining named real parameters of the
+ function. Specifically, ``ssl_context`` is expected
+ to be included with SSL.Context; if it is not
+ default ``'sslv23'`` is substituted).
+ """
+ HTTPSConnection.__init__(self, host, port, strict, **ssl)
+
+ self._username = username.encode('utf8') \
+ if isinstance(username, six.string_types) else username
+ self._password = password.encode('utf8') \
+ if isinstance(password, six.string_types) else password
+ self._proxy_auth = None # type: str
+ self._proxy_UA = None # type: str
+
+ def putrequest(self, method, url, skip_host=0, skip_accept_encoding=0):
+ # type: (AnyStr, AnyStr, int, int) -> None
+ """
+ putrequest is called before connect, so can interpret url and get
+ real host/port to be used to make CONNECT request to proxy
+ """
+ proto, netloc, path, query, fragment = urlsplit(url)
+ if not proto:
+ raise ValueError("unknown URL type: %s" % url)
+
+ # get host & port
+ try:
+ username_password, host_port = netloc.split('@')
+ except ValueError:
+ host_port = netloc
+
+ try:
+ host, port_s = host_port.split(':')
+ port = int(port_s)
+ except ValueError:
+ host = host_port
+ # try to get port from proto
+ try:
+ port = self._ports[proto]
+ except KeyError:
+ raise ValueError("unknown protocol for: %s" % url)
+
+ self._real_host = host # type: str
+ self._real_port = port # type: int
+ rest = urlunsplit(('', '', path, query, fragment))
+ HTTPSConnection.putrequest(self, method, rest, skip_host,
+ skip_accept_encoding)
+
+ def putheader(self, header, value):
+ # type: (AnyStr, AnyStr) -> None
+ # Store the auth header if passed in.
+ if header.lower() == self._UA_HEADER.lower():
+ self._proxy_UA = value
+ if header.lower() == self._AUTH_HEADER.lower():
+ self._proxy_auth = value
+ else:
+ HTTPSConnection.putheader(self, header, value)
+
+ def endheaders(self, *args, **kwargs):
+ # type: (*Any, **Any) -> None
+ # We've recieved all of hte headers. Use the supplied username
+ # and password for authorization, possibly overriding the authstring
+ # supplied in the headers.
+ if not self._proxy_auth:
+ self._proxy_auth = self._encode_auth()
+
+ HTTPSConnection.endheaders(self, *args, **kwargs)
+
+ def connect(self):
+ # type: () -> None
+ HTTPConnection.connect(self)
+
+ # send proxy CONNECT request
+ self.sock.sendall(self._get_connect_msg())
+ response = HTTPResponse(self.sock)
+ response.begin()
+
+ code = response.status
+ if code != 200:
+ # proxy returned and error, abort connection, and raise exception
+ self.close()
+ raise socket.error("Proxy connection failed: %d" % code)
+
+ self._start_ssl()
+
+ def _get_connect_msg(self):
+ # type: () -> bytes
+ """ Return an HTTP CONNECT request to send to the proxy. """
+ msg = "CONNECT %s:%d HTTP/1.1\r\n" % (self._real_host, self._real_port)
+ msg = msg + "Host: %s:%d\r\n" % (self._real_host, self._real_port)
+ if self._proxy_UA:
+ msg = msg + "%s: %s\r\n" % (self._UA_HEADER, self._proxy_UA)
+ if self._proxy_auth:
+ msg = msg + "%s: %s\r\n" % (self._AUTH_HEADER, self._proxy_auth)
+ msg = msg + "\r\n"
+ return six.ensure_binary(msg)
+
+ def _start_ssl(self):
+ # type: () -> None
+ """ Make this connection's socket SSL-aware. """
+ self.sock = SSL.Connection(self.ssl_ctx, self.sock)
+ self.sock.setup_ssl()
+ self.sock.set_connect_state()
+ self.sock.connect_ssl()
+
+ def _encode_auth(self):
+ # type: () -> Optional[bytes]
+ """ Encode the username and password for use in the auth header. """
+ if not (self._username and self._password):
+ return None
+ # Authenticated proxy
+ userpass = "%s:%s" % (self._username, self._password)
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ enc_userpass = base64.encodestring(userpass).replace("\n", "")
+ return six.ensure_binary("Basic %s" % enc_userpass)