【问题标题】:Is it possible to identify TLS info. in requests response?是否可以识别 TLS 信息。在请求响应中?
【发布时间】:2019-08-21 04:42:59
【问题描述】:

我正在使用 python 的请求模块。我可以得到服务器的响应头和应用层数据:

import requests
r = requests.get('https://yahoo.com')
print(r.url)  

我的问题:请求是否允许检索传输层数据(服务器的 TLS 选择版本、密码套件等?)。

【问题讨论】:

    标签: ssl python-requests python-3.6


    【解决方案1】:

    这是一个快速的丑陋猴子补丁版本:

    import requests
    from requests.packages.urllib3.connection import VerifiedHTTPSConnection
    
    SOCK = None
    
    _orig_connect = requests.packages.urllib3.connection.VerifiedHTTPSConnection.connect
    
    def _connect(self):
        global SOCK
        _orig_connect(self)
        SOCK = self.sock
    
    requests.packages.urllib3.connection.VerifiedHTTPSConnection.connect = _connect
    
    requests.get('https://yahoo.com')
    tlscon = SOCK.connection
    print 'Cipher is %s/%s' % (tlscon.get_cipher_name(), tlscon.get_cipher_version())
    print 'Remote certificates: %s' % (tlscon.get_peer_certificate())
    print 'Protocol version: %s' % tlscon.get_protocol_version_name()
    

    这会产生:

    Cipher is ECDHE-RSA-AES128-GCM-SHA256/TLSv1.2
    Remote certificates: <OpenSSL.crypto.X509 object at 0x10c60e310>
    Protocol version: TLSv1.2
    

    但是这很糟糕,因为猴子修补并依赖于唯一的全局变量,这也意味着您无法检查重定向步骤中发生的情况等等。

    也许有一些工作可以变成Transport Adapter,以获取底层连接作为请求的属性(可能是会话或其他东西)。不过,这可能会造成泄漏,因为在当前实现中,底层套接字会被尽快丢弃(参见 How to get the underlying socket when using Python requests)。

    更新,现在使用传输适配器

    这可行,并且符合框架(没有全局变量,应该处理重定向等。不过,代理可能需要做一些事情,比如也为 proxy_manager_for 添加覆盖),但它还有很多代码。

    import requests
    from requests.adapters import HTTPAdapter
    from requests.packages.urllib3.connectionpool import HTTPSConnectionPool
    from requests.packages.urllib3.poolmanager import PoolManager
    
    
    class InspectedHTTPSConnectionPool(HTTPSConnectionPool):
        @property
        def inspector(self):
            return self._inspector
    
        @inspector.setter
        def inspector(self, inspector):
            self._inspector = inspector
    
        def _validate_conn(self, conn):
            r = super(InspectedHTTPSConnectionPool, self)._validate_conn(conn)
            if self.inspector:
                self.inspector(self.host, self.port, conn)
    
            return r
    
    
    class InspectedPoolManager(PoolManager):
        @property
        def inspector(self):
            return self._inspector
    
        @inspector.setter
        def inspector(self, inspector):
            self._inspector = inspector
    
        def _new_pool(self, scheme, host, port):
            if scheme != 'https':
                return super(InspectedPoolManager, self)._new_pool(scheme, host, port)
    
            kwargs = self.connection_pool_kw
            if scheme == 'http':
                kwargs = self.connection_pool_kw.copy()
                for kw in SSL_KEYWORDS:
                    kwargs.pop(kw, None)
    
            pool = InspectedHTTPSConnectionPool(host, port, **kwargs)
            pool.inspector = self.inspector
            return pool
    
    
    class TLSInspectorAdapter(HTTPAdapter):
        def __init__(self, inspector):
            self._inspector = inspector
            super(TLSInspectorAdapter, self).__init__()
    
        def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs):
            self.poolmanager = InspectedPoolManager(num_pools=connections, maxsize=maxsize, block=block, strict=True, **pool_kwargs)
            self.poolmanager.inspector = self._inspector
    
    
    def connection_inspector(host, port, connection):
        print 'host is %s' % host
        print 'port is %s' % port
        print 'connection is %s' % connection
        sock = connection.sock
        sock_connection = sock.connection
        print 'socket is %s' % sock
        print 'Protocol version: %s' % sock_connection.get_protocol_version_name()
        print 'Cipher is %s/%s' % (sock_connection.get_cipher_name(), sock_connection.get_cipher_version())
        print 'Remote certificate: %s' % sock.getpeercert()
    
    
    
    url = 'https://yahoo.com'
    s = requests.Session()
    s.mount(url, TLSInspectorAdapter(connection_inspector))
    r = s.get(url)
    

    是的,socketconnection 之间的命名有很多混淆:请求使用具有一组连接的“连接池”,实际上对于 HTTPS 来说,是一个 PyOpenSSL WrappedSocket,它具有本身是一个底层的真实 TLS 连接(即 PyOpenSSL Connection 对象)。因此connection_inspector中的奇怪形式。

    但这会返回预期的结果:

    host is yahoo.com
    port is 443
    connection is <requests.packages.urllib3.connection.VerifiedHTTPSConnection object at 0x10bb372d0>
    socket is <requests.packages.urllib3.contrib.pyopenssl.WrappedSocket object at 0x10bb37410>
    Protocol version: TLSv1.2
    Cipher is ECDHE-RSA-AES128-GCM-SHA256/TLSv1.2
    Remote certificate: {'subjectAltName': [('DNS', '*.www.yahoo.com'), ('DNS', 'add.my.yahoo.com'), ('DNS', '*.amp.yimg.com'), ('DNS', 'au.yahoo.com'), ('DNS', 'be.yahoo.com'), ('DNS', 'br.yahoo.com'), ('DNS', 'ca.my.yahoo.com'), ('DNS', 'ca.rogers.yahoo.com'), ('DNS', 'ca.yahoo.com'), ('DNS', 'ddl.fp.yahoo.com'), ('DNS', 'de.yahoo.com'), ('DNS', 'en-maktoob.yahoo.com'), ('DNS', 'espanol.yahoo.com'), ('DNS', 'es.yahoo.com'), ('DNS', 'fr-be.yahoo.com'), ('DNS', 'fr-ca.rogers.yahoo.com'), ('DNS', 'frontier.yahoo.com'), ('DNS', 'fr.yahoo.com'), ('DNS', 'gr.yahoo.com'), ('DNS', 'hk.yahoo.com'), ('DNS', 'hsrd.yahoo.com'), ('DNS', 'ideanetsetter.yahoo.com'), ('DNS', 'id.yahoo.com'), ('DNS', 'ie.yahoo.com'), ('DNS', 'in.yahoo.com'), ('DNS', 'it.yahoo.com'), ('DNS', 'maktoob.yahoo.com'), ('DNS', 'malaysia.yahoo.com'), ('DNS', 'mbp.yimg.com'), ('DNS', 'my.yahoo.com'), ('DNS', 'nz.yahoo.com'), ('DNS', 'ph.yahoo.com'), ('DNS', 'qc.yahoo.com'), ('DNS', 'ro.yahoo.com'), ('DNS', 'se.yahoo.com'), ('DNS', 'sg.yahoo.com'), ('DNS', 'tw.yahoo.com'), ('DNS', 'uk.yahoo.com'), ('DNS', 'us.yahoo.com'), ('DNS', 'verizon.yahoo.com'), ('DNS', 'vn.yahoo.com'), ('DNS', 'www.yahoo.com'), ('DNS', 'yahoo.com'), ('DNS', 'za.yahoo.com')], 'subject': ((('commonName', u'*.www.yahoo.com'),),)}
    

    其他想法:

    1. 如果您像在https://stackoverflow.com/a/22253656/6368697 中使用基本上poolmanager.pool_classes_by_scheme['http'] = MyHTTPConnectionPool 那样进行猴子修补,您可能会删除很多代码;但这仍然是猴子补丁,遗憾的是 PoolManager 没有为 pool_classes_by_scheme 变量提供一个很好的 API 以便能够轻松覆盖它
    2. PyOpenSSL ssl_context 可能能够保存将在 TLS 握手期间调用的回调并获取底层数据;然后在init_poolmanager 中,您只需要在调用超类之前在kwargs 中设置ssl_context;这个例子在https://gist.github.com/aiguofer/1eb881ccf199d4aaa2097d87f93ace6a ssl.create_default_context 和ssl 远没有PyOpenSSL 强大,我看不到使用ssl 添加回调的方法,它们存在于PyOpenSSL。 YMMV。

    PS:

    1. 一旦你发现你有_validate_conn,你可以覆盖它,因为它得到了正确的连接对象,生活会更轻松
    2. 尤其是如果您正确地在顶部进行导入,您需要使用分布在请求中的 urllib3 包,而不是“真正的”urllib3,否则您会遇到很多奇怪的错误,因为两者中的相同方法不会有相同的签名...

    【讨论】:

      猜你喜欢
      • 2012-09-11
      • 2010-09-14
      • 1970-01-01
      • 1970-01-01
      • 2016-03-22
      • 2011-10-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多