9 changed files with 1421 additions and 0 deletions
@ -0,0 +1,19 @@ |
|||
"""ndg_httpsclient - PyOpenSSL utility to make a httplib-like interface suitable |
|||
for use with urllib2 |
|||
|
|||
This is a setuptools namespace_package. DO NOT place any other |
|||
code in this file! There is no guarantee that it will be installed |
|||
with easy_install. See: |
|||
|
|||
http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages |
|||
|
|||
... for details. |
|||
""" |
|||
__author__ = "P J Kershaw" |
|||
__date__ = "06/01/12" |
|||
__copyright__ = "(C) 2012 Science and Technology Facilities Council" |
|||
__license__ = "BSD - see LICENSE file in top-level directory" |
|||
__contact__ = "Philip.Kershaw@stfc.ac.uk" |
|||
__revision__ = '$Id$' |
|||
|
|||
__import__('pkg_resources').declare_namespace(__name__) |
@ -0,0 +1,9 @@ |
|||
"""ndg_httpsclient - PyOpenSSL utility to make a httplib-like interface suitable |
|||
for use with urllib2 |
|||
""" |
|||
__author__ = "P J Kershaw (STFC) and Richard Wilkinson (Tessella)" |
|||
__date__ = "09/12/11" |
|||
__copyright__ = "(C) 2011 Science and Technology Facilities Council" |
|||
__license__ = "BSD - see LICENSE file in top-level directory" |
|||
__contact__ = "Philip.Kershaw@stfc.ac.uk" |
|||
__revision__ = '$Id$' |
@ -0,0 +1,131 @@ |
|||
"""ndg_httpsclient HTTPS module containing PyOpenSSL implementation of |
|||
httplib.HTTPSConnection |
|||
|
|||
PyOpenSSL utility to make a httplib-like interface suitable for use with |
|||
urllib2 |
|||
""" |
|||
__author__ = "P J Kershaw (STFC)" |
|||
__date__ = "09/12/11" |
|||
__copyright__ = "(C) 2012 Science and Technology Facilities Council" |
|||
__license__ = "BSD - see LICENSE file in top-level directory" |
|||
__contact__ = "Philip.Kershaw@stfc.ac.uk" |
|||
__revision__ = '$Id$' |
|||
import logging |
|||
import socket |
|||
import sys |
|||
|
|||
if sys.version_info[0] > 2: |
|||
from http.client import HTTPS_PORT |
|||
from http.client import HTTPConnection |
|||
|
|||
from urllib.request import AbstractHTTPHandler |
|||
else: |
|||
from httplib import HTTPS_PORT |
|||
from httplib import HTTPConnection |
|||
|
|||
from urllib2 import AbstractHTTPHandler |
|||
|
|||
|
|||
from OpenSSL import SSL |
|||
|
|||
from ndg.httpsclient.ssl_socket import SSLSocket |
|||
|
|||
log = logging.getLogger(__name__) |
|||
|
|||
|
|||
class HTTPSConnection(HTTPConnection): |
|||
"""This class allows communication via SSL using PyOpenSSL. |
|||
It is based on httplib.HTTPSConnection, modified to use PyOpenSSL. |
|||
|
|||
Note: This uses the constructor inherited from HTTPConnection to allow it to |
|||
be used with httplib and HTTPSContextHandler. To use the class directly with |
|||
an SSL context set ssl_context after construction. |
|||
|
|||
@cvar default_port: default port for this class (443) |
|||
@type default_port: int |
|||
@cvar default_ssl_method: default SSL method used if no SSL context is |
|||
explicitly set - defaults to version 2/3. |
|||
@type default_ssl_method: int |
|||
""" |
|||
default_port = HTTPS_PORT |
|||
default_ssl_method = SSL.SSLv23_METHOD |
|||
|
|||
def __init__(self, host, port=None, strict=None, |
|||
timeout=socket._GLOBAL_DEFAULT_TIMEOUT, ssl_context=None): |
|||
HTTPConnection.__init__(self, host, port, strict, timeout) |
|||
if not hasattr(self, 'ssl_context'): |
|||
self.ssl_context = None |
|||
|
|||
if ssl_context is not None: |
|||
if not isinstance(ssl_context, SSL.Context): |
|||
raise TypeError('Expecting OpenSSL.SSL.Context type for "' |
|||
'ssl_context" keyword; got %r instead' % |
|||
ssl_context) |
|||
|
|||
self.ssl_context = ssl_context |
|||
|
|||
def connect(self): |
|||
"""Create SSL socket and connect to peer |
|||
""" |
|||
if getattr(self, 'ssl_context', None): |
|||
if not isinstance(self.ssl_context, SSL.Context): |
|||
raise TypeError('Expecting OpenSSL.SSL.Context type for "' |
|||
'ssl_context" attribute; got %r instead' % |
|||
self.ssl_context) |
|||
ssl_context = self.ssl_context |
|||
else: |
|||
ssl_context = SSL.Context(self.__class__.default_ssl_method) |
|||
|
|||
sock = socket.create_connection((self.host, self.port), self.timeout) |
|||
|
|||
# Tunnel if using a proxy - ONLY available for Python 2.6.2 and above |
|||
if getattr(self, '_tunnel_host', None): |
|||
self.sock = sock |
|||
self._tunnel() |
|||
|
|||
self.sock = SSLSocket(ssl_context, sock) |
|||
|
|||
# Go to client mode. |
|||
self.sock.set_connect_state() |
|||
|
|||
def close(self): |
|||
"""Close socket and shut down SSL connection""" |
|||
if hasattr(self.sock, "close"): |
|||
self.sock.close() |
|||
|
|||
|
|||
class HTTPSContextHandler(AbstractHTTPHandler): |
|||
'''HTTPS handler that allows a SSL context to be set for the SSL |
|||
connections. |
|||
''' |
|||
https_request = AbstractHTTPHandler.do_request_ |
|||
|
|||
def __init__(self, ssl_context, debuglevel=0): |
|||
""" |
|||
@param ssl_context:SSL context |
|||
@type ssl_context: OpenSSL.SSL.Context |
|||
@param debuglevel: debug level for HTTPSHandler |
|||
@type debuglevel: int |
|||
""" |
|||
AbstractHTTPHandler.__init__(self, debuglevel) |
|||
|
|||
if ssl_context is not None: |
|||
if not isinstance(ssl_context, SSL.Context): |
|||
raise TypeError('Expecting OpenSSL.SSL.Context type for "' |
|||
'ssl_context" keyword; got %r instead' % |
|||
ssl_context) |
|||
self.ssl_context = ssl_context |
|||
else: |
|||
self.ssl_context = SSL.Context(SSL.TLSv1_METHOD) |
|||
|
|||
def https_open(self, req): |
|||
"""Opens HTTPS request |
|||
@param req: HTTP request |
|||
@return: HTTP Response object |
|||
""" |
|||
# Make a custom class extending HTTPSConnection, with the SSL context |
|||
# set as a class variable so that it is available to the connect method. |
|||
customHTTPSContextConnection = type('CustomHTTPSContextConnection', |
|||
(HTTPSConnection, object), |
|||
{'ssl_context': self.ssl_context}) |
|||
return self.do_open(customHTTPSContextConnection, req) |
@ -0,0 +1,98 @@ |
|||
"""ndg_httpsclient SSL Context utilities module containing convenience routines |
|||
for setting SSL context configuration. |
|||
|
|||
""" |
|||
__author__ = "P J Kershaw (STFC)" |
|||
__date__ = "09/12/11" |
|||
__copyright__ = "(C) 2012 Science and Technology Facilities Council" |
|||
__license__ = "BSD - see LICENSE file in top-level directory" |
|||
__contact__ = "Philip.Kershaw@stfc.ac.uk" |
|||
__revision__ = '$Id$' |
|||
import sys |
|||
|
|||
if sys.version_info[0] > 2: |
|||
import urllib.parse as urlparse_ |
|||
else: |
|||
import urlparse as urlparse_ |
|||
|
|||
from OpenSSL import SSL |
|||
|
|||
from ndg.httpsclient.ssl_peer_verification import ServerSSLCertVerification |
|||
|
|||
|
|||
class SSlContextConfig(object): |
|||
""" |
|||
Holds configuration options for creating a SSL context. This is used as a |
|||
template to create the contexts with specific verification callbacks. |
|||
""" |
|||
def __init__(self, key_file=None, cert_file=None, pem_file=None, ca_dir=None, |
|||
verify_peer=False): |
|||
self.key_file = key_file |
|||
self.cert_file = cert_file |
|||
self.pem_file = pem_file |
|||
self.ca_dir = ca_dir |
|||
self.verify_peer = verify_peer |
|||
|
|||
|
|||
def make_ssl_context_from_config(ssl_config=False, url=None): |
|||
return make_ssl_context(ssl_config.key_file, ssl_config.cert_file, |
|||
ssl_config.pem_file, ssl_config.ca_dir, |
|||
ssl_config.verify_peer, url) |
|||
|
|||
|
|||
def make_ssl_context(key_file=None, cert_file=None, pem_file=None, ca_dir=None, |
|||
verify_peer=False, url=None, method=SSL.TLSv1_METHOD, |
|||
key_file_passphrase=None): |
|||
""" |
|||
Creates SSL context containing certificate and key file locations. |
|||
""" |
|||
ssl_context = SSL.Context(method) |
|||
|
|||
# Key file defaults to certificate file if present. |
|||
if cert_file: |
|||
ssl_context.use_certificate_file(cert_file) |
|||
|
|||
if key_file_passphrase: |
|||
passwd_cb = lambda max_passphrase_len, set_prompt, userdata: \ |
|||
key_file_passphrase |
|||
ssl_context.set_passwd_cb(passwd_cb) |
|||
|
|||
if key_file: |
|||
ssl_context.use_privatekey_file(key_file) |
|||
elif cert_file: |
|||
ssl_context.use_privatekey_file(cert_file) |
|||
|
|||
if pem_file or ca_dir: |
|||
ssl_context.load_verify_locations(pem_file, ca_dir) |
|||
|
|||
def _callback(conn, x509, errnum, errdepth, preverify_ok): |
|||
"""Default certification verification callback. |
|||
Performs no checks and returns the status passed in. |
|||
""" |
|||
return preverify_ok |
|||
|
|||
verify_callback = _callback |
|||
|
|||
if verify_peer: |
|||
ssl_context.set_verify_depth(9) |
|||
if url: |
|||
set_peer_verification_for_url_hostname(ssl_context, url) |
|||
else: |
|||
ssl_context.set_verify(SSL.VERIFY_PEER, verify_callback) |
|||
else: |
|||
ssl_context.set_verify(SSL.VERIFY_NONE, verify_callback) |
|||
|
|||
return ssl_context |
|||
|
|||
|
|||
def set_peer_verification_for_url_hostname(ssl_context, url, |
|||
if_verify_enabled=False): |
|||
'''Convenience routine to set peer verification callback based on |
|||
ServerSSLCertVerification class''' |
|||
if not if_verify_enabled or (ssl_context.get_verify_mode() & SSL.VERIFY_PEER): |
|||
urlObj = urlparse_.urlparse(url) |
|||
hostname = urlObj.hostname |
|||
server_ssl_cert_verif = ServerSSLCertVerification(hostname=hostname) |
|||
verify_callback_ = server_ssl_cert_verif.get_verify_server_cert_func() |
|||
ssl_context.set_verify(SSL.VERIFY_PEER, verify_callback_) |
|||
|
@ -0,0 +1,237 @@ |
|||
"""ndg_httpsclient - module containing SSL peer verification class. |
|||
""" |
|||
__author__ = "P J Kershaw (STFC)" |
|||
__date__ = "09/12/11" |
|||
__copyright__ = "(C) 2012 Science and Technology Facilities Council" |
|||
__license__ = "BSD - see LICENSE file in top-level directory" |
|||
__contact__ = "Philip.Kershaw@stfc.ac.uk" |
|||
__revision__ = '$Id$' |
|||
import re |
|||
import logging |
|||
log = logging.getLogger(__name__) |
|||
|
|||
try: |
|||
from ndg.httpsclient.subj_alt_name import SubjectAltName |
|||
from pyasn1.codec.der import decoder as der_decoder |
|||
SUBJ_ALT_NAME_SUPPORT = True |
|||
|
|||
except ImportError as e: |
|||
SUBJ_ALT_NAME_SUPPORT = False |
|||
SUBJ_ALT_NAME_SUPPORT_MSG = ( |
|||
'SubjectAltName support is disabled - check pyasn1 package ' |
|||
'installation to enable' |
|||
) |
|||
import warnings |
|||
warnings.warn(SUBJ_ALT_NAME_SUPPORT_MSG) |
|||
|
|||
|
|||
class ServerSSLCertVerification(object): |
|||
"""Check server identity. If hostname doesn't match, allow match of |
|||
host's Distinguished Name against server DN setting""" |
|||
DN_LUT = { |
|||
'commonName': 'CN', |
|||
'organisationalUnitName': 'OU', |
|||
'organisation': 'O', |
|||
'countryName': 'C', |
|||
'emailAddress': 'EMAILADDRESS', |
|||
'localityName': 'L', |
|||
'stateOrProvinceName': 'ST', |
|||
'streetAddress': 'STREET', |
|||
'domainComponent': 'DC', |
|||
'userid': 'UID' |
|||
} |
|||
SUBJ_ALT_NAME_EXT_NAME = 'subjectAltName' |
|||
PARSER_RE_STR = '/(%s)=' % '|'.join(list(DN_LUT.keys()) + list(DN_LUT.values())) |
|||
PARSER_RE = re.compile(PARSER_RE_STR) |
|||
|
|||
__slots__ = ('__hostname', '__certDN', '__subj_alt_name_match') |
|||
|
|||
def __init__(self, certDN=None, hostname=None, subj_alt_name_match=True): |
|||
"""Override parent class __init__ to enable setting of certDN |
|||
setting |
|||
|
|||
@type certDN: string |
|||
@param certDN: Set the expected Distinguished Name of the |
|||
server to avoid errors matching hostnames. This is useful |
|||
where the hostname is not fully qualified |
|||
@type hostname: string |
|||
@param hostname: hostname to match against peer certificate |
|||
subjectAltNames or subject common name |
|||
@type subj_alt_name_match: bool |
|||
@param subj_alt_name_match: flag to enable/disable matching of hostname |
|||
against peer certificate subjectAltNames. Nb. A setting of True will |
|||
be ignored if the pyasn1 package is not installed |
|||
""" |
|||
self.__certDN = None |
|||
self.__hostname = None |
|||
|
|||
if certDN is not None: |
|||
self.certDN = certDN |
|||
|
|||
if hostname is not None: |
|||
self.hostname = hostname |
|||
|
|||
if subj_alt_name_match: |
|||
if not SUBJ_ALT_NAME_SUPPORT: |
|||
log.warning('Overriding "subj_alt_name_match" keyword setting: ' |
|||
'peer verification with subjectAltNames is disabled') |
|||
self.__subj_alt_name_match = False |
|||
else: |
|||
self.__subj_alt_name_match = True |
|||
else: |
|||
log.debug('Disabling peer verification with subject ' |
|||
'subjectAltNames!') |
|||
self.__subj_alt_name_match = False |
|||
|
|||
def __call__(self, connection, peerCert, errorStatus, errorDepth, |
|||
preverifyOK): |
|||
"""Verify server certificate |
|||
|
|||
@type connection: OpenSSL.SSL.Connection |
|||
@param connection: SSL connection object |
|||
@type peerCert: basestring |
|||
@param peerCert: server host certificate as OpenSSL.crypto.X509 |
|||
instance |
|||
@type errorStatus: int |
|||
@param errorStatus: error status passed from caller. This is the value |
|||
returned by the OpenSSL C function X509_STORE_CTX_get_error(). Look-up |
|||
x509_vfy.h in the OpenSSL source to get the meanings of the different |
|||
codes. PyOpenSSL doesn't help you! |
|||
@type errorDepth: int |
|||
@param errorDepth: a non-negative integer representing where in the |
|||
certificate chain the error occurred. If it is zero it occured in the |
|||
end entity certificate, one if it is the certificate which signed the |
|||
end entity certificate and so on. |
|||
|
|||
@type preverifyOK: int |
|||
@param preverifyOK: the error status - 0 = Error, 1 = OK of the current |
|||
SSL context irrespective of any verification checks done here. If this |
|||
function yields an OK status, it should enforce the preverifyOK value |
|||
so that any error set upstream overrides and is honoured. |
|||
@rtype: int |
|||
@return: status code - 0/False = Error, 1/True = OK |
|||
""" |
|||
if peerCert.has_expired(): |
|||
# Any expired certificate in the chain should result in an error |
|||
log.error('Certificate %r in peer certificate chain has expired', |
|||
peerCert.get_subject()) |
|||
|
|||
return False |
|||
|
|||
elif errorDepth == 0: |
|||
# Only interested in DN of last certificate in the chain - this must |
|||
# match the expected Server DN setting |
|||
peerCertSubj = peerCert.get_subject() |
|||
peerCertDN = peerCertSubj.get_components() |
|||
peerCertDN.sort() |
|||
|
|||
if self.certDN is None: |
|||
# Check hostname against peer certificate CN field instead: |
|||
if self.hostname is None: |
|||
log.error('No "hostname" or "certDN" set to check peer ' |
|||
'certificate against') |
|||
return False |
|||
|
|||
# Check for subject alternative names |
|||
if self.__subj_alt_name_match: |
|||
dns_names = self._get_subj_alt_name(peerCert) |
|||
if self.hostname in dns_names: |
|||
return preverifyOK |
|||
|
|||
# If no subjectAltNames, default to check of subject Common Name |
|||
if peerCertSubj.commonName == self.hostname: |
|||
return preverifyOK |
|||
else: |
|||
log.error('Peer certificate CN %r doesn\'t match the ' |
|||
'expected CN %r', peerCertSubj.commonName, |
|||
self.hostname) |
|||
return False |
|||
else: |
|||
if peerCertDN == self.certDN: |
|||
return preverifyOK |
|||
else: |
|||
log.error('Peer certificate DN %r doesn\'t match the ' |
|||
'expected DN %r', peerCertDN, self.certDN) |
|||
return False |
|||
else: |
|||
return preverifyOK |
|||
|
|||
def get_verify_server_cert_func(self): |
|||
def verify_server_cert(connection, peerCert, errorStatus, errorDepth, |
|||
preverifyOK): |
|||
return self.__call__(connection, peerCert, errorStatus, |
|||
errorDepth, preverifyOK) |
|||
|
|||
return verify_server_cert |
|||
|
|||
@classmethod |
|||
def _get_subj_alt_name(cls, peer_cert): |
|||
'''Extract subjectAltName DNS name settings from certificate extensions |
|||
|
|||
@param peer_cert: peer certificate in SSL connection. subjectAltName |
|||
settings if any will be extracted from this |
|||
@type peer_cert: OpenSSL.crypto.X509 |
|||
''' |
|||
# Search through extensions |
|||
dns_name = [] |
|||
general_names = SubjectAltName() |
|||
for i in range(peer_cert.get_extension_count()): |
|||
ext = peer_cert.get_extension(i) |
|||
ext_name = ext.get_short_name() |
|||
if ext_name == cls.SUBJ_ALT_NAME_EXT_NAME: |
|||
# PyOpenSSL returns extension data in ASN.1 encoded form |
|||
ext_dat = ext.get_data() |
|||
decoded_dat = der_decoder.decode(ext_dat, |
|||
asn1Spec=general_names) |
|||
|
|||
for name in decoded_dat: |
|||
if isinstance(name, SubjectAltName): |
|||
for entry in range(len(name)): |
|||
component = name.getComponentByPosition(entry) |
|||
dns_name.append(str(component.getComponent())) |
|||
|
|||
return dns_name |
|||
|
|||
def _getCertDN(self): |
|||
return self.__certDN |
|||
|
|||
def _setCertDN(self, val): |
|||
if isinstance(val, str): |
|||
# Allow for quoted DN |
|||
certDN = val.strip('"') |
|||
|
|||
dnFields = self.__class__.PARSER_RE.split(certDN) |
|||
if len(dnFields) < 2: |
|||
raise TypeError('Error parsing DN string: "%s"' % certDN) |
|||
|
|||
self.__certDN = list(zip(dnFields[1::2], dnFields[2::2])) |
|||
self.__certDN.sort() |
|||
|
|||
elif not isinstance(val, list): |
|||
for i in val: |
|||
if not len(i) == 2: |
|||
raise TypeError('Expecting list of two element DN field, ' |
|||
'DN field value pairs for "certDN" ' |
|||
'attribute') |
|||
self.__certDN = val |
|||
else: |
|||
raise TypeError('Expecting list or string type for "certDN" ' |
|||
'attribute') |
|||
|
|||
certDN = property(fget=_getCertDN, |
|||
fset=_setCertDN, |
|||
doc="Distinguished Name for Server Certificate") |
|||
|
|||
# Get/Set Property methods |
|||
def _getHostname(self): |
|||
return self.__hostname |
|||
|
|||
def _setHostname(self, val): |
|||
if not isinstance(val, str): |
|||
raise TypeError("Expecting string type for hostname " |
|||
"attribute") |
|||
self.__hostname = val |
|||
|
|||
hostname = property(fget=_getHostname, |
|||
fset=_setHostname, |
|||
doc="hostname of server") |
@ -0,0 +1,282 @@ |
|||
"""PyOpenSSL utilities including HTTPSSocket class which wraps PyOpenSSL |
|||
SSL connection into a httplib-like interface suitable for use with urllib2 |
|||
|
|||
""" |
|||
__author__ = "P J Kershaw" |
|||
__date__ = "21/12/10" |
|||
__copyright__ = "(C) 2012 Science and Technology Facilities Council" |
|||
__license__ = "BSD - see LICENSE file in top-level directory" |
|||
__contact__ = "Philip.Kershaw@stfc.ac.uk" |
|||
__revision__ = '$Id$' |
|||
|
|||
from datetime import datetime |
|||
import logging |
|||
import socket |
|||
from io import BytesIO |
|||
|
|||
from OpenSSL import SSL |
|||
|
|||
log = logging.getLogger(__name__) |
|||
|
|||
|
|||
class SSLSocket(object): |
|||
"""SSL Socket class wraps pyOpenSSL's SSL.Connection class implementing |
|||
the makefile method so that it is compatible with the standard socket |
|||
interface and usable with httplib. |
|||
|
|||
@cvar default_buf_size: default buffer size for recv operations in the |
|||
makefile method |
|||
@type default_buf_size: int |
|||
""" |
|||
default_buf_size = 8192 |
|||
|
|||
def __init__(self, ctx, sock=None): |
|||
"""Create SSL socket object |
|||
|
|||
@param ctx: SSL context |
|||
@type ctx: OpenSSL.SSL.Context |
|||
@param sock: underlying socket object |
|||
@type sock: socket.socket |
|||
""" |
|||
if sock is not None: |
|||
self.socket = sock |
|||
else: |
|||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
|||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
|||
|
|||
self.__ssl_conn = SSL.Connection(ctx, self.socket) |
|||
self.buf_size = self.__class__.default_buf_size |
|||
self._makefile_refs = 0 |
|||
|
|||
def __del__(self): |
|||
"""Close underlying socket when this object goes out of scope |
|||
""" |
|||
self.close() |
|||
|
|||
@property |
|||
def buf_size(self): |
|||
"""Buffer size for makefile method recv() operations""" |
|||
return self.__buf_size |
|||
|
|||
@buf_size.setter |
|||
def buf_size(self, value): |
|||
"""Buffer size for makefile method recv() operations""" |
|||
if not isinstance(value, int): |
|||
raise TypeError('Expecting int type for "buf_size"; ' |
|||
'got %r instead' % type(value)) |
|||
self.__buf_size = value |
|||
|
|||
def close(self): |
|||
"""Shutdown the SSL connection and call the close method of the |
|||
underlying socket""" |
|||
if self._makefile_refs < 1: |
|||
try: |
|||
self.__ssl_conn.shutdown() |
|||
except (SSL.Error, SSL.SysCallError): |
|||
# Make errors on shutdown non-fatal |
|||
pass |
|||
else: |
|||
self._makefile_refs -= 1 |
|||
|
|||
def set_shutdown(self, mode): |
|||
"""Set the shutdown state of the Connection. |
|||
@param mode: bit vector of either or both of SENT_SHUTDOWN and |
|||
RECEIVED_SHUTDOWN |
|||
""" |
|||
self.__ssl_conn.set_shutdown(mode) |
|||
|
|||
def get_shutdown(self): |
|||
"""Get the shutdown state of the Connection. |
|||
@return: bit vector of either or both of SENT_SHUTDOWN and |
|||
RECEIVED_SHUTDOWN |
|||
""" |
|||
return self.__ssl_conn.get_shutdown() |
|||
|
|||
def bind(self, addr): |
|||
"""bind to the given address - calls method of the underlying socket |
|||
@param addr: address/port number tuple |
|||
@type addr: tuple""" |
|||
self.__ssl_conn.bind(addr) |
|||
|
|||
def listen(self, backlog): |
|||
"""Listen for connections made to the socket. |
|||
|
|||
@param backlog: specifies the maximum number of queued connections and |
|||
should be at least 1; the maximum value is system-dependent (usually 5). |
|||
@param backlog: int |
|||
""" |
|||
self.__ssl_conn.listen(backlog) |
|||
|
|||
def set_accept_state(self): |
|||
"""Set the connection to work in server mode. The handshake will be |
|||
handled automatically by read/write""" |
|||
self.__ssl_conn.set_accept_state() |
|||
|
|||
def accept(self): |
|||
"""Accept an SSL connection. |
|||
|
|||
@return: pair (ssl, addr) where ssl is a new SSL connection object and |
|||
addr is the address bound to the other end of the SSL connection. |
|||
@rtype: tuple |
|||
""" |
|||
return self.__ssl_conn.accept() |
|||
|
|||
def set_connect_state(self): |
|||
"""Set the connection to work in client mode. The handshake will be |
|||
handled automatically by read/write""" |
|||
self.__ssl_conn.set_connect_state() |
|||
|
|||
def connect(self, addr): |
|||
"""Call the connect method of the underlying socket and set up SSL on |
|||
the socket, using the Context object supplied to this Connection object |
|||
at creation. |
|||
|
|||
@param addr: address/port number pair |
|||
@type addr: tuple |
|||
""" |
|||
self.__ssl_conn.connect(addr) |
|||
|
|||
def shutdown(self, how): |
|||
"""Send the shutdown message to the Connection. |
|||
|
|||
@param how: for socket.socket this flag determines whether read, write |
|||
or both type operations are supported. OpenSSL.SSL.Connection doesn't |
|||
support this so this parameter is IGNORED |
|||
@return: true if the shutdown message exchange is completed and false |
|||
otherwise (in which case you call recv() or send() when the connection |
|||
becomes readable/writeable. |
|||
@rtype: bool |
|||
""" |
|||
return self.__ssl_conn.shutdown() |
|||
|
|||
def renegotiate(self): |
|||
"""Renegotiate this connection's SSL parameters.""" |
|||
return self.__ssl_conn.renegotiate() |
|||
|
|||
def pending(self): |
|||
"""@return: numbers of bytes that can be safely read from the SSL |
|||
buffer. |
|||
@rtype: int |
|||
""" |
|||
return self.__ssl_conn.pending() |
|||
|
|||
def send(self, data, *flags_arg): |
|||
"""Send data to the socket. Nb. The optional flags argument is ignored. |
|||
- retained for compatibility with socket.socket interface |
|||
|
|||
@param data: data to send down the socket |
|||
@type data: string |
|||
""" |
|||
return self.__ssl_conn.send(data) |
|||
|
|||
def sendall(self, data): |
|||
self.__ssl_conn.sendall(data) |
|||
|
|||
def recv(self, size=default_buf_size): |
|||
"""Receive data from the Connection. |
|||
|
|||
@param size: The maximum amount of data to be received at once |
|||
@type size: int |
|||
@return: data received. |
|||
@rtype: string |
|||
""" |
|||
return self.__ssl_conn.recv(size) |
|||
|
|||
def setblocking(self, mode): |
|||
"""Set this connection's underlying socket blocking _mode_. |
|||
|
|||
@param mode: blocking mode |
|||
@type mode: int |
|||
""" |
|||
self.__ssl_conn.setblocking(mode) |
|||
|
|||
def fileno(self): |
|||
""" |
|||
@return: file descriptor number for the underlying socket |
|||
@rtype: int |
|||
""" |
|||
return self.__ssl_conn.fileno() |
|||
|
|||
def getsockopt(self, *args): |
|||
"""See socket.socket.getsockopt |
|||
""" |
|||
return self.__ssl_conn.getsockopt(*args) |
|||
|
|||
def setsockopt(self, *args): |
|||
"""See socket.socket.setsockopt |
|||
|
|||
@return: value of the given socket option |
|||
@rtype: int/string |
|||
""" |
|||
return self.__ssl_conn.setsockopt(*args) |
|||
|
|||
def state_string(self): |
|||
"""Return the SSL state of this connection.""" |
|||
return self.__ssl_conn.state_string() |
|||
|
|||
def makefile(self, *args): |
|||
"""Specific to Python socket API and required by httplib: convert |
|||
response into a file-like object. This implementation reads using recv |
|||
and copies the output into a StringIO buffer to simulate a file object |
|||
for consumption by httplib |
|||
|
|||
Nb. Ignoring optional file open mode (StringIO is generic and will |
|||
open for read and write unless a string is passed to the constructor) |
|||
and buffer size - httplib set a zero buffer size which results in recv |
|||
reading nothing |
|||
|
|||
@return: file object for data returned from socket |
|||
@rtype: cStringIO.StringO |
|||
""" |
|||
self._makefile_refs += 1 |
|||
|
|||
# Optimisation |
|||
_buf_size = self.buf_size |
|||
|
|||
i=0 |
|||
stream = BytesIO() |
|||
startTime = datetime.utcnow() |
|||
try: |
|||
dat = self.__ssl_conn.recv(_buf_size) |
|||
while dat: |
|||
i+=1 |
|||
stream.write(dat) |
|||
dat = self.__ssl_conn.recv(_buf_size) |
|||
|
|||
except (SSL.ZeroReturnError, SSL.SysCallError): |
|||
# Connection is closed - assuming here that all is well and full |
|||
# response has been received. httplib will catch an error in |
|||
# incomplete content since it checks the content-length header |
|||
# against the actual length of data received |
|||
pass |
|||
|
|||
if log.getEffectiveLevel() <= logging.DEBUG: |
|||
log.debug("Socket.makefile %d recv calls completed in %s", i, |
|||
datetime.utcnow() - startTime) |
|||
|
|||
# Make sure to rewind the buffer otherwise consumers of the content will |
|||
# read from the end of the buffer |
|||
stream.seek(0) |
|||
|
|||
return stream |
|||
|
|||
def getsockname(self): |
|||
""" |
|||
@return: the socket's own address |
|||
@rtype: |
|||
""" |
|||
return self.__ssl_conn.getsockname() |
|||
|
|||
def getpeername(self): |
|||
""" |
|||
@return: remote address to which the socket is connected |
|||
""" |
|||
return self.__ssl_conn.getpeername() |
|||
|
|||
def get_context(self): |
|||
'''Retrieve the Context object associated with this Connection. ''' |
|||
return self.__ssl_conn.get_context() |
|||
|
|||
def get_peer_certificate(self): |
|||
'''Retrieve the other side's certificate (if any) ''' |
|||
return self.__ssl_conn.get_peer_certificate() |
@ -0,0 +1,153 @@ |
|||
"""NDG HTTPS Client package |
|||
|
|||
Use pyasn1 to provide support for parsing ASN.1 formatted subjectAltName |
|||
content for SSL peer verification. Code based on: |
|||
|
|||
http://stackoverflow.com/questions/5519958/how-do-i-parse-subjectaltname-extension-data-using-pyasn1 |
|||
""" |
|||
__author__ = "P J Kershaw" |
|||
__date__ = "01/02/12" |
|||
__copyright__ = "(C) 2012 Science and Technology Facilities Council" |
|||
__license__ = "BSD - see LICENSE file in top-level directory" |
|||
__contact__ = "Philip.Kershaw@stfc.ac.uk" |
|||
__revision__ = '$Id$' |
|||
try: |
|||
from pyasn1.type import univ, constraint, char, namedtype, tag |
|||
|
|||
except ImportError as e: |
|||
import_error_msg = ('Error importing pyasn1, subjectAltName check for SSL ' |
|||
'peer verification will be disabled. Import error ' |
|||
'is: %s' % e) |
|||
import warnings |
|||
warnings.warn(import_error_msg) |
|||
class Pyasn1ImportError(ImportError): |
|||
"Raise for pyasn1 import error" |
|||
raise Pyasn1ImportError(import_error_msg) |
|||
|
|||
|
|||
MAX = 64 |
|||
|
|||
|
|||
class DirectoryString(univ.Choice): |
|||
"""ASN.1 Directory string class""" |
|||
componentType = namedtype.NamedTypes( |
|||
namedtype.NamedType( |
|||
'teletexString', char.TeletexString().subtype( |
|||
subtypeSpec=constraint.ValueSizeConstraint(1, MAX))), |
|||
namedtype.NamedType( |
|||
'printableString', char.PrintableString().subtype( |
|||
subtypeSpec=constraint.ValueSizeConstraint(1, MAX))), |
|||
namedtype.NamedType( |
|||
'universalString', char.UniversalString().subtype( |
|||
subtypeSpec=constraint.ValueSizeConstraint(1, MAX))), |
|||
namedtype.NamedType( |
|||
'utf8String', char.UTF8String().subtype( |
|||
subtypeSpec=constraint.ValueSizeConstraint(1, MAX))), |
|||
namedtype.NamedType( |
|||
'bmpString', char.BMPString().subtype( |
|||
subtypeSpec=constraint.ValueSizeConstraint(1, MAX))), |
|||
namedtype.NamedType( |
|||
'ia5String', char.IA5String().subtype( |
|||
subtypeSpec=constraint.ValueSizeConstraint(1, MAX))), |
|||
) |
|||
|
|||
|
|||
class AttributeValue(DirectoryString): |
|||
"""ASN.1 Attribute value""" |
|||
|
|||
|
|||
class AttributeType(univ.ObjectIdentifier): |
|||
"""ASN.1 Attribute type""" |
|||
|
|||
|
|||
class AttributeTypeAndValue(univ.Sequence): |
|||
"""ASN.1 Attribute type and value class""" |
|||
componentType = namedtype.NamedTypes( |
|||
namedtype.NamedType('type', AttributeType()), |
|||
namedtype.NamedType('value', AttributeValue()), |
|||
) |
|||
|
|||
|
|||
class RelativeDistinguishedName(univ.SetOf): |
|||
'''ASN.1 Realtive distinguished name''' |
|||
componentType = AttributeTypeAndValue() |
|||
|
|||
class RDNSequence(univ.SequenceOf): |
|||
'''ASN.1 RDN sequence class''' |
|||
componentType = RelativeDistinguishedName() |
|||
|
|||
|
|||
class Name(univ.Choice): |
|||
'''ASN.1 name class''' |
|||
componentType = namedtype.NamedTypes( |
|||
namedtype.NamedType('', RDNSequence()), |
|||
) |
|||
|
|||
|
|||
class Extension(univ.Sequence): |
|||
'''ASN.1 extension class''' |
|||
componentType = namedtype.NamedTypes( |
|||
namedtype.NamedType('extnID', univ.ObjectIdentifier()), |
|||
namedtype.DefaultedNamedType('critical', univ.Boolean('False')), |
|||
namedtype.NamedType('extnValue', univ.OctetString()), |
|||
) |
|||
|
|||
|
|||
class Extensions(univ.SequenceOf): |
|||
'''ASN.1 extensions class''' |
|||
componentType = Extension() |
|||
sizeSpec = univ.SequenceOf.sizeSpec + constraint.ValueSizeConstraint(1, MAX) |
|||
|
|||
|
|||
class AnotherName(univ.Sequence): |
|||
componentType = namedtype.NamedTypes( |
|||
namedtype.NamedType('type-id', univ.ObjectIdentifier()), |
|||
namedtype.NamedType('value', univ.Any().subtype( |
|||
explicitTag=tag.Tag(tag.tagClassContext, |
|||
tag.tagFormatSimple, 0))) |
|||
) |
|||
|
|||
|
|||
class GeneralName(univ.Choice): |
|||
'''ASN.1 configuration for X.509 certificate subjectAltNames fields''' |
|||
componentType = namedtype.NamedTypes( |
|||
namedtype.NamedType('otherName', AnotherName().subtype( |
|||
implicitTag=tag.Tag(tag.tagClassContext, |
|||
tag.tagFormatSimple, 0))), |
|||
namedtype.NamedType('rfc822Name', char.IA5String().subtype( |
|||
implicitTag=tag.Tag(tag.tagClassContext, |
|||
tag.tagFormatSimple, 1))), |
|||
namedtype.NamedType('dNSName', char.IA5String().subtype( |
|||
implicitTag=tag.Tag(tag.tagClassContext, |
|||
tag.tagFormatSimple, 2))), |
|||
# namedtype.NamedType('x400Address', ORAddress().subtype( |
|||
# implicitTag=tag.Tag(tag.tagClassContext, |
|||
# tag.tagFormatSimple, 3))), |
|||
namedtype.NamedType('directoryName', Name().subtype( |
|||
implicitTag=tag.Tag(tag.tagClassContext, |
|||
tag.tagFormatSimple, 4))), |
|||
# namedtype.NamedType('ediPartyName', EDIPartyName().subtype( |
|||
# implicitTag=tag.Tag(tag.tagClassContext, |
|||
# tag.tagFormatSimple, 5))), |
|||
namedtype.NamedType('uniformResourceIdentifier', char.IA5String().subtype( |
|||
implicitTag=tag.Tag(tag.tagClassContext, |
|||
tag.tagFormatSimple, 6))), |
|||
namedtype.NamedType('iPAddress', univ.OctetString().subtype( |
|||
implicitTag=tag.Tag(tag.tagClassContext, |
|||
tag.tagFormatSimple, 7))), |
|||
namedtype.NamedType('registeredID', univ.ObjectIdentifier().subtype( |
|||
implicitTag=tag.Tag(tag.tagClassContext, |
|||
tag.tagFormatSimple, 8))), |
|||
) |
|||
|
|||
|
|||
class GeneralNames(univ.SequenceOf): |
|||
'''Sequence of names for ASN.1 subjectAltNames settings''' |
|||
componentType = GeneralName() |
|||
sizeSpec = univ.SequenceOf.sizeSpec + constraint.ValueSizeConstraint(1, MAX) |
|||
|
|||
|
|||
class SubjectAltName(GeneralNames): |
|||
'''ASN.1 implementation for subjectAltNames support''' |
|||
|
|||
|
@ -0,0 +1,78 @@ |
|||
"""urllib2 style build opener integrates with HTTPSConnection class from this |
|||
package. |
|||
""" |
|||
__author__ = "P J Kershaw" |
|||
__date__ = "21/12/10" |
|||
__copyright__ = "(C) 2011 Science and Technology Facilities Council" |
|||
__license__ = "BSD - see LICENSE file in top-level directory" |
|||
__contact__ = "Philip.Kershaw@stfc.ac.uk" |
|||
__revision__ = '$Id$' |
|||
import logging |
|||
import sys |
|||
|
|||
# Py 2 <=> 3 compatibility for class type checking |
|||
if sys.version_info[0] > 2: |
|||
class_type_ = type |
|||
from urllib.request import (ProxyHandler, UnknownHandler, |
|||
HTTPDefaultErrorHandler, FTPHandler, |
|||
FileHandler, HTTPErrorProcessor, |
|||
HTTPHandler, OpenerDirector, |
|||
HTTPRedirectHandler) |
|||
else: |
|||
import types |
|||
class_type_ = types.ClassType |
|||
|
|||
from urllib2 import (ProxyHandler, UnknownHandler, HTTPDefaultErrorHandler, |
|||
FTPHandler, FileHandler, HTTPErrorProcessor, |
|||
HTTPHandler, OpenerDirector, HTTPRedirectHandler) |
|||
|
|||
from ndg.httpsclient.https import HTTPSContextHandler |
|||
|
|||
log = logging.getLogger(__name__) |
|||
|
|||
|
|||
# Copied from urllib2 with modifications for ssl |
|||
def build_opener(*handlers, **kw): |
|||
"""Create an opener object from a list of handlers. |
|||
|
|||
The opener will use several default handlers, including support |
|||
for HTTP and FTP. |
|||
|
|||
If any of the handlers passed as arguments are subclasses of the |
|||
default handlers, the default handlers will not be used. |
|||
""" |
|||
def isclass(obj): |
|||
return isinstance(obj, class_type_) or hasattr(obj, "__bases__") |
|||
|
|||
opener = OpenerDirector() |
|||
default_classes = [ProxyHandler, UnknownHandler, HTTPHandler, |
|||
HTTPDefaultErrorHandler, HTTPRedirectHandler, |
|||
FTPHandler, FileHandler, HTTPErrorProcessor] |
|||
check_classes = list(default_classes) |
|||
check_classes.append(HTTPSContextHandler) |
|||
skip = [] |
|||
for klass in check_classes: |
|||
for check in handlers: |
|||
if isclass(check): |
|||
if issubclass(check, klass): |
|||
skip.append(klass) |
|||
elif isinstance(check, klass): |
|||
skip.append(klass) |
|||
|
|||
for klass in default_classes: |
|||
if klass not in skip: |
|||
opener.add_handler(klass()) |
|||
|
|||
# Pick up SSL context from keyword settings |
|||
ssl_context = kw.get('ssl_context') |
|||
|
|||
# Add the HTTPS handler with ssl_context |
|||
if HTTPSContextHandler not in skip: |
|||
opener.add_handler(HTTPSContextHandler(ssl_context)) |
|||
|
|||
for h in handlers: |
|||
if isclass(h): |
|||
h = h() |
|||
opener.add_handler(h) |
|||
|
|||
return opener |
@ -0,0 +1,414 @@ |
|||
"""Utilities using NDG HTTPS Client, including a main module that can be used to |
|||
fetch from a URL. |
|||
""" |
|||
__author__ = "R B Wilkinson" |
|||
__date__ = "09/12/11" |
|||
__copyright__ = "(C) 2011 Science and Technology Facilities Council" |
|||
__license__ = "BSD - see LICENSE file in top-level directory" |
|||
__contact__ = "Philip.Kershaw@stfc.ac.uk" |
|||
__revision__ = '$Id$' |
|||
|
|||
import logging |
|||
from optparse import OptionParser |
|||
import os |
|||
import sys |
|||
|
|||
if sys.version_info[0] > 2: |
|||
import http.cookiejar as cookiejar_ |
|||
import http.client as http_client_ |
|||
from urllib.request import Request as Request_ |
|||
from urllib.request import HTTPHandler as HTTPHandler_ |
|||
from urllib.request import HTTPCookieProcessor as HTTPCookieProcessor_ |
|||
from urllib.request import HTTPBasicAuthHandler as HTTPBasicAuthHandler_ |
|||
from urllib.request import HTTPPasswordMgrWithDefaultRealm as \ |
|||
HTTPPasswordMgrWithDefaultRealm_ |
|||
from urllib.request import ProxyHandler as ProxyHandler_ |
|||
from urllib.error import HTTPError as HTTPError_ |
|||
import urllib.parse as urlparse_ |
|||
else: |
|||
import cookielib as cookiejar_ |
|||
import httplib as http_client_ |
|||
from urllib2 import Request as Request_ |
|||
from urllib2 import HTTPHandler as HTTPHandler_ |
|||
from urllib2 import HTTPCookieProcessor as HTTPCookieProcessor_ |
|||
from urllib2 import HTTPBasicAuthHandler as HTTPBasicAuthHandler_ |
|||
from urllib2 import HTTPPasswordMgrWithDefaultRealm as \ |
|||
HTTPPasswordMgrWithDefaultRealm_ |
|||
from urllib2 import ProxyHandler as ProxyHandler_ |
|||
from urllib2 import HTTPError as HTTPError_ |
|||
import urlparse as urlparse_ |
|||
|
|||
from ndg.httpsclient.urllib2_build_opener import build_opener |
|||
from ndg.httpsclient.https import HTTPSContextHandler |
|||
from ndg.httpsclient import ssl_context_util |
|||
|
|||
log = logging.getLogger(__name__) |
|||
|
|||
class AccumulatingHTTPCookieProcessor(HTTPCookieProcessor_): |
|||
"""Cookie processor that adds new cookies (instead of replacing the existing |
|||
ones as HTTPCookieProcessor does) |
|||
""" |
|||
def http_request(self, request): |
|||
"""Processes cookies for a HTTP request. |
|||
@param request: request to process |
|||
@type request: urllib2.Request |
|||
@return: request |
|||
@rtype: urllib2.Request |
|||
""" |
|||
COOKIE_HEADER_NAME = "Cookie" |
|||
tmp_request = Request_(request.get_full_url(), request.data, {}, |
|||
request.origin_req_host, |
|||
request.unverifiable) |
|||
self.cookiejar.add_cookie_header(tmp_request) |
|||
# Combine existing and new cookies. |
|||
new_cookies = tmp_request.get_header(COOKIE_HEADER_NAME) |
|||
if new_cookies: |
|||
if request.has_header(COOKIE_HEADER_NAME): |
|||
# Merge new cookies with existing ones. |
|||
old_cookies = request.get_header(COOKIE_HEADER_NAME) |
|||
merged_cookies = '; '.join([old_cookies, new_cookies]) |
|||
request.add_unredirected_header(COOKIE_HEADER_NAME, |
|||
merged_cookies) |
|||
else: |
|||
# No existing cookies so just set new ones. |
|||
request.add_unredirected_header(COOKIE_HEADER_NAME, new_cookies) |
|||
return request |
|||
|
|||
# Process cookies for HTTPS in the same way. |
|||
https_request = http_request |
|||
|
|||
|
|||
class URLFetchError(Exception): |
|||
"""Error fetching content from URL""" |
|||
|
|||
|
|||
def fetch_from_url(url, config, data=None, handlers=None): |
|||
"""Returns data retrieved from a URL. |
|||
@param url: URL to attempt to open |
|||
@type url: basestring |
|||
@param config: SSL context configuration |
|||
@type config: Configuration |
|||
@return data retrieved from URL or None |
|||
""" |
|||
return_code, return_message, response = open_url(url, config, data=data, |
|||
handlers=handlers) |
|||
if return_code and return_code == http_client_.OK: |
|||
return_data = response.read() |
|||
response.close() |
|||
return return_data |
|||
else: |
|||
raise URLFetchError(return_message) |
|||
|
|||
def fetch_from_url_to_file(url, config, output_file, data=None, handlers=None): |
|||
"""Writes data retrieved from a URL to a file. |
|||
@param url: URL to attempt to open |
|||
@type url: basestring |
|||
@param config: SSL context configuration |
|||
@type config: Configuration |
|||
@param output_file: output file |
|||
@type output_file: basestring |
|||
@return: tuple ( |
|||
returned HTTP status code or 0 if an error occurred |
|||
returned message |
|||
boolean indicating whether access was successful) |
|||
""" |
|||
return_code, return_message, response = open_url(url, config, data=data, |
|||
handlers=handlers) |
|||
if return_code == http_client_.OK: |
|||
return_data = response.read() |
|||
response.close() |
|||
outfile = open(output_file, "w") |
|||
outfile.write(return_data) |
|||
outfile.close() |
|||
|
|||
return return_code, return_message, return_code == http_client_.OK |
|||
|
|||
|
|||
def fetch_stream_from_url(url, config, data=None, handlers=None): |
|||
"""Returns data retrieved from a URL. |
|||
@param url: URL to attempt to open |
|||
@type url: basestring |
|||
@param config: SSL context configuration |
|||
@type config: Configuration |
|||
@param data: HTTP POST data |
|||
@type data: str |
|||
@param handlers: list of custom urllib2 handlers to add to the request |
|||
@type handlers: iterable |
|||
@return: data retrieved from URL or None |
|||
@rtype: file derived type |
|||
""" |
|||
return_code, return_message, response = open_url(url, config, data=data, |
|||
handlers=handlers) |
|||
if return_code and return_code == http_client_.OK: |
|||
return response |
|||
else: |
|||
raise URLFetchError(return_message) |
|||
|
|||
|
|||
def open_url(url, config, data=None, handlers=None): |
|||
"""Attempts to open a connection to a specified URL. |
|||
@param url: URL to attempt to open |
|||
@param config: SSL context configuration |
|||
@type config: Configuration |
|||
@param data: HTTP POST data |
|||
@type data: str |
|||
@param handlers: list of custom urllib2 handlers to add to the request |
|||
@type handlers: iterable |
|||
@return: tuple ( |
|||
returned HTTP status code or 0 if an error occurred |
|||
returned message or error description |
|||
response object) |
|||
""" |
|||
debuglevel = 1 if config.debug else 0 |
|||
|
|||
# Set up handlers for URL opener. |
|||
if config.cookie: |
|||
cj = config.cookie |
|||
else: |
|||
cj = cookiejar_.CookieJar() |
|||
|
|||
# Use a cookie processor that accumulates cookies when redirects occur so |
|||
# that an application can redirect for authentication and retain both any |
|||
# cookies for the application and the security system (c.f., |
|||
# urllib2.HTTPCookieProcessor which replaces cookies). |
|||
cookie_handler = AccumulatingHTTPCookieProcessor(cj) |
|||
|
|||
if not handlers: |
|||
handlers = [] |
|||
|
|||
handlers.append(cookie_handler) |
|||
|
|||
if config.debug: |
|||
http_handler = HTTPHandler_(debuglevel=debuglevel) |
|||
https_handler = HTTPSContextHandler(config.ssl_context, |
|||
debuglevel=debuglevel) |
|||
handlers.extend([http_handler, https_handler]) |
|||
|
|||
if config.http_basicauth: |
|||
# currently only supports http basic auth |
|||
auth_handler = HTTPBasicAuthHandler_(HTTPPasswordMgrWithDefaultRealm_()) |
|||
auth_handler.add_password(realm=None, uri=url, |
|||
user=config.httpauth[0], |
|||
passwd=config.httpauth[1]) |
|||
handlers.append(auth_handler) |
|||
|
|||
|
|||
# Explicitly remove proxy handling if the host is one listed in the value of |
|||
# the no_proxy environment variable because urllib2 does use proxy settings |
|||
# set via http_proxy and https_proxy, but does not take the no_proxy value |
|||
# into account. |
|||
if not _should_use_proxy(url, config.no_proxy): |
|||
handlers.append(ProxyHandler_({})) |
|||
log.debug("Not using proxy") |
|||
elif config.proxies: |
|||
handlers.append(ProxyHandler_(config.proxies)) |
|||
log.debug("Configuring proxies: %s" % config.proxies) |
|||
|
|||
opener = build_opener(*handlers, ssl_context=config.ssl_context) |
|||
|
|||
headers = config.headers |
|||
if headers is None: |
|||
headers = {} |
|||
|
|||
request = Request_(url, data, headers) |
|||
|
|||
# Open the URL and check the response. |
|||
return_code = 0 |
|||
return_message = '' |
|||
response = None |
|||
|
|||
# FIXME |
|||
response = opener.open(request) |
|||
|
|||
try: |
|||
response = opener.open(request) |
|||
return_message = response.msg |
|||
return_code = response.code |
|||
if log.isEnabledFor(logging.DEBUG): |
|||
for index, cookie in enumerate(cj): |
|||
log.debug("%s : %s", index, cookie) |
|||
|
|||
except HTTPError_ as exc: |
|||
return_code = exc.code |
|||
return_message = "Error: %s" % exc.msg |
|||
if log.isEnabledFor(logging.DEBUG): |
|||
log.debug("%s %s", exc.code, exc.msg) |
|||
|
|||
except Exception as exc: |
|||
return_message = "Error: %s" % exc.__str__() |
|||
if log.isEnabledFor(logging.DEBUG): |
|||
import traceback |
|||
log.debug(traceback.format_exc()) |
|||
|
|||
return (return_code, return_message, response) |
|||
|
|||
|
|||
def _should_use_proxy(url, no_proxy=None): |
|||
"""Determines whether a proxy should be used to open a connection to the |
|||
specified URL, based on the value of the no_proxy environment variable. |
|||
@param url: URL |
|||
@type url: basestring or urllib2.Request |
|||
""" |
|||
if no_proxy is None: |
|||
no_proxy_effective = os.environ.get('no_proxy', '') |
|||
else: |
|||
no_proxy_effective = no_proxy |
|||
|
|||
urlObj = urlparse_.urlparse(_url_as_string(url)) |
|||
for np in [h.strip() for h in no_proxy_effective.split(',')]: |
|||
if urlObj.hostname == np: |
|||
return False |
|||
|
|||
return True |
|||
|
|||
def _url_as_string(url): |
|||
"""Returns the URL string from a URL value that is either a string or |
|||
urllib2.Request.. |
|||
@param url: URL |
|||
@type url: basestring or urllib2.Request |
|||
@return: URL string |
|||
@rtype: basestring |
|||
""" |
|||
if isinstance(url, Request_): |
|||
return url.get_full_url() |
|||
elif isinstance(url, str): |
|||
return url |
|||
else: |
|||
raise TypeError("Expected type %r or %r" % |
|||
(str, Request_)) |
|||
|
|||
|
|||
class Configuration(object): |
|||
"""Connection configuration. |
|||
""" |
|||
def __init__(self, ssl_context, debug=False, proxies=None, no_proxy=None, |
|||
cookie=None, http_basicauth=None, headers=None): |
|||
""" |
|||
@param ssl_context: SSL context to use with this configuration |
|||
@type ssl_context: OpenSSL.SSL.Context |
|||
@param debug: if True, output debugging information |
|||
@type debug: bool |
|||
@param proxies: proxies to use for |
|||
@type proxies: dict with basestring keys and values |
|||
@param no_proxy: hosts for which a proxy should not be used |
|||
@type no_proxy: basestring |
|||
@param cookie: cookies to set for request |
|||
@type cookie: cookielib.CookieJar (python 3 - http.cookiejar) |
|||
@param http_basicauth: http authentication, or None |
|||
@type http_basicauth: tuple of (username,password) |
|||
@param headers: http headers |
|||
@type headers: dict |
|||
""" |
|||
self.ssl_context = ssl_context |
|||
self.debug = debug |
|||
self.proxies = proxies |
|||
self.no_proxy = no_proxy |
|||
self.cookie = cookie |
|||
self.http_basicauth = http_basicauth |
|||
self.headers = headers |
|||
|
|||
|
|||
def main(): |
|||
'''Utility to fetch data using HTTP or HTTPS GET from a specified URL. |
|||
''' |
|||
parser = OptionParser(usage="%prog [options] url") |
|||
parser.add_option("-c", "--certificate", dest="cert_file", metavar="FILE", |
|||
default=os.path.expanduser("~/credentials.pem"), |
|||
help="Certificate file - defaults to $HOME/credentials.pem") |
|||
parser.add_option("-k", "--private-key", dest="key_file", metavar="FILE", |
|||
default=None, |
|||
help="Private key file - defaults to the certificate file") |
|||
parser.add_option("-t", "--ca-certificate-dir", dest="ca_dir", |
|||
metavar="PATH", |
|||
default=None, |
|||
help="Trusted CA certificate file directory") |
|||
parser.add_option("-d", "--debug", action="store_true", dest="debug", |
|||
default=False, |
|||
help="Print debug information.") |
|||
parser.add_option("-p", "--post-data-file", dest="data_file", |
|||
metavar="FILE", default=None, |
|||
help="POST data file") |
|||
parser.add_option("-f", "--fetch", dest="output_file", metavar="FILE", |
|||
default=None, help="Output file") |
|||
parser.add_option("-n", "--no-verify-peer", action="store_true", |
|||
dest="no_verify_peer", default=False, |
|||
help="Skip verification of peer certificate.") |
|||
parser.add_option("-a", "--basicauth", dest="basicauth", |
|||
metavar="USER:PASSWD", |
|||
default=None, |
|||
help="HTTP authentication credentials") |
|||
parser.add_option("--header", action="append", dest="headers", |
|||
metavar="HEADER: VALUE", |
|||
help="Add HTTP header to request") |
|||
(options, args) = parser.parse_args() |
|||
if len(args) != 1: |
|||
parser.error("Incorrect number of arguments") |
|||
|
|||
url = args[0] |
|||
|
|||
if options.debug: |
|||
logging.getLogger().setLevel(logging.DEBUG) |
|||
|
|||
if options.key_file and os.path.exists(options.key_file): |
|||
key_file = options.key_file |
|||
else: |
|||
key_file = None |
|||
|
|||
if options.cert_file and os.path.exists(options.cert_file): |
|||
cert_file = options.cert_file |
|||
else: |
|||
cert_file = None |
|||
|
|||
if options.ca_dir and os.path.exists(options.ca_dir): |
|||
ca_dir = options.ca_dir |
|||
else: |
|||
ca_dir = None |
|||
|
|||
verify_peer = not options.no_verify_peer |
|||
|
|||
if options.data_file and os.path.exists(options.data_file): |
|||
data_file = open(options.data_file) |
|||
data = data_file.read() |
|||
data_file.close() |
|||
else: |
|||
data = None |
|||
|
|||
if options.basicauth: |
|||
http_basicauth = options.basicauth.split(':', 1) |
|||
else: |
|||
http_basicauth = None |
|||
|
|||
headers = {} |
|||
if options.headers: |
|||
for h in options.headers: |
|||
key, val = h.split(':', 1) |
|||
headers[key.strip()] = val.lstrip() |
|||
|
|||
# If a private key file is not specified, the key is assumed to be stored in |
|||
# the certificate file. |
|||
ssl_context = ssl_context_util.make_ssl_context(key_file, |
|||
cert_file, |
|||
None, |
|||
ca_dir, |
|||
verify_peer, |
|||
url) |
|||
|
|||
config = Configuration(ssl_context, |
|||
options.debug, |
|||
http_basicauth=http_basicauth, |
|||
headers=headers) |
|||
if options.output_file: |
|||
return_code, return_message = fetch_from_url_to_file( |
|||
url, |
|||
config, |
|||
options.output_file, |
|||
data)[:2] |
|||
raise SystemExit(return_code, return_message) |
|||
else: |
|||
data = fetch_from_url(url, config) |
|||
print(data) |
|||
|
|||
|
|||
if __name__=='__main__': |
|||
logging.basicConfig() |
|||
main() |
Loading…
Reference in new issue