Можно ли идентифицировать информацию TLS.в ответах на запросы? - PullRequest
1 голос
/ 30 марта 2019

Я использую модуль запросов Python.Я могу получить заголовки ответа сервера и данные прикладного уровня в виде:

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

Мой вопрос: позволяют ли запросы получать данные транспортного уровня (выбранная версия TLS сервера, набор шифров и т. Д.?).

1 Ответ

1 голос
/ 01 апреля 2019

Вот быстрая версия уродливого исправления обезьяны, которая работает:

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, чтобы получить базовое соединение как свойство запроса (вероятно из сессии или что-то).Это может привести к утечкам, поскольку в текущей реализации базовый сокет отбрасывается как можно быстрее (см. Как получить базовый сокет при использовании запросов Python ).

Обновление, теперьиспользование транспортного адаптера

Это работает и соответствует структуре (нет глобальной переменной, она должна обрабатывать перенаправления и т. д., хотя может быть что-то для прокси-серверов, например, добавление переопределения для 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'),),)}

Другие идеи:

  1. Вы можете удалить много кода, еслиВы делаете патч обезьяны, как в https://stackoverflow.com/a/22253656/6368697, в основном poolmanager.pool_classes_by_scheme['http'] = MyHTTPConnectionPool;но это по-прежнему непонятное исправление, и грустно, что PoolManager не предоставляет хороший API для переменной pool_classes_by_scheme, чтобы можно было легко ее переопределить
  2. PyOpenSSL ssl_context может содержать обратные вызовы, которые будут вызваныво время рукопожатия TLS и получения базовых данных;затем в init_poolmanager вам нужно просто настроить ssl_context в kwargs перед вызовом суперкласса;этот пример в https://gist.github.com/aiguofer/1eb881ccf199d4aaa2097d87f93ace6a <=, а может и нет, потому что на самом деле структура взята из <code>ssl.create_default_context, а ssl гораздо менее мощен, чем PyOpenSSL, и я не вижу способа добавить обратные вызовы, используя ssl, гдеони существуют для PyOpenSSL.YMMV.

PS:

  1. Как только вы обнаружите, что у вас есть _validate_conn, который вы можете переопределить при получении нужного объекта соединения, жизнь становится проще
  2. И особенно, если вы делаете импорт сверху, вам нужно использовать пакеты urllib3, которые распространяются внутри запросов, а не «реальный» urllib3, иначе вы получите много странных ошибок, потому что одни и те же методы в обоих не имеютодни и те же подписи ...
...