我正在使用python的请求模块。我可以通过以下方式获取服务器的响应标头和应用程序层数据:
import requests
r = requests.get('https://yahoo.com')
print(r.url)
我的问题:请求是否允许检索传输层数据(服务器的TLS选定版本,密码套件等?)。
答案 0 :(得分: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)
是的,socket
和connection
之间的命名存在很多混乱:请求使用具有一组连接的“连接池”,实际上对于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'),),)}
其他想法:
poolmanager.pool_classes_by_scheme['http'] = MyHTTPConnectionPool
做猴子补丁,则可能会删除很多代码;但这仍然是猴子修补程序,可悲的是PoolManager并没有为pool_classes_by_scheme
变量提供一个很好的API以便能够轻松地覆盖它init_poolmanager
中,您只需要在调用超类之前在kwargs
中设置ssl_context即可; https://gist.github.com/aiguofer/1eb881ccf199d4aaa2097d87f93ace6a中的该示例<=或可能不是,因为实际上该结构来自ssl.create_default_context
,而ssl
的功能远不及PyOpenSSL
,而且我看不到使用以下方法添加回调的方法ssl
,它们在PyOpenSSL
中存在。 YMMV。PS:
_validate_conn
可以被覆盖,因为它获得了正确的连接对象,因此生活变得更加轻松