Туннелирование HTTPS-прокси с модулем ssl

Я хотел бы вручную (используя сокет и ssl модули) делают HTTPS запрос через прокси, который сам использует HTTPS.

Я могу нормально выполнить начальный обмен CONNECT:

import ssl, socket

PROXY_ADDR = ("proxy-addr", 443)
CONNECT = "CONNECT example.com:443 HTTP/1.1\r\n\r\n"

sock = socket.create_connection(PROXY_ADDR)
sock = ssl.wrap_socket(sock)
sock.sendall(CONNECT)
s = ""
while s[-4:] != "\r\n\r\n":
    s += sock.recv(1)
print repr(s)

Приведенный выше код печатает HTTP/1.1 200 Connection established плюс некоторые заголовки, чего я и ожидаю. Итак, теперь я должен быть готов сделать запрос, например.

sock.sendall("GET / HTTP/1.1\r\n\r\n")

но приведенный выше код возвращает

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>400 Bad Request</title>
</head><body>
<h1>Bad Request</h1>
<p>Your browser sent a request that this server could not understand.<br />
Reason: You're speaking plain HTTP to an SSL-enabled server port.<br />
Instead use the HTTPS scheme to access this URL, please.<br />
</body></html>

Это тоже имеет смысл, так как мне все еще нужно выполнить рукопожатие SSL с сервером example.com, к которому я туннелирую. Однако, если вместо немедленной отправки запроса GET я скажу

sock = ssl.wrap_socket(sock)

чтобы выполнить рукопожатие с удаленным сервером, я получаю исключение:

Traceback (most recent call last):
  File "so_test.py", line 18, in <module>
    ssl.wrap_socket(sock)
  File "/usr/lib/python2.6/ssl.py", line 350, in wrap_socket
    suppress_ragged_eofs=suppress_ragged_eofs)
  File "/usr/lib/python2.6/ssl.py", line 118, in __init__
    self.do_handshake()
  File "/usr/lib/python2.6/ssl.py", line 293, in do_handshake
    self._sslobj.do_handshake()
ssl.SSLError: [Errno 1] _ssl.c:480: error:140770FC:SSL routines:SSL23_GET_SERVER_HELLO:unknown protocol

Итак, как я могу выполнить SSL-рукопожатие с удаленным сервером example.com?

РЕДАКТИРОВАТЬ: я почти уверен, что перед моим вторым вызовом wrap_socket дополнительные данные недоступны, потому что вызов sock.recv(1) блокируется на неопределенный срок.


person Eli Courtwright    schedule 08.12.2010    source источник
comment
мое грубое предположение заключается в том, что ssl.wrap_socket заботится о состоянии подключения к сокету. обычно вы создаете сокет, затем обертываете его, а затем подключаетесь. Здесь вы создаете сокет, подключаетесь, затем обертываете. возможно, ssl просто сбит с толку уже подключенным базовым состоянием сокета. github.com/kennethreitz/requests/ блоб/   -  person Dima Tisnek    schedule 25.11.2013
comment
эй, тебе повезло? Я застрял с той же проблемой, но тоже ничего не нашел...   -  person 02strich    schedule 26.11.2013


Ответы (5)


Это должно работать, если строка CONNECT переписана следующим образом:

CONNECT = "CONNECT %s:%s HTTP/1.0\r\nConnection: close\r\n\r\n" % (server, port)

Не уверен, почему это работает, но, возможно, это как-то связано с используемым мной прокси. Вот пример кода:

from OpenSSL import SSL
import socket

def verify_cb(conn, cert, errun, depth, ok):
        return True

server = 'mail.google.com'
port = 443
PROXY_ADDR = ("proxy.example.com", 3128)
CONNECT = "CONNECT %s:%s HTTP/1.0\r\nConnection: close\r\n\r\n" % (server, port)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(PROXY_ADDR)
s.send(CONNECT)
print s.recv(4096)      

ctx = SSL.Context(SSL.SSLv23_METHOD)
ctx.set_verify(SSL.VERIFY_PEER, verify_cb)
ss = SSL.Connection(ctx, s)

ss.set_connect_state()
ss.do_handshake()
cert = ss.get_peer_certificate()
print cert.get_subject()
ss.shutdown()
ss.close()

Обратите внимание, как сначала открывается сокет, а затем открытый сокет помещается в контекст SSL. Затем я вручную инициализирую рукопожатие SSL. И вывод:

HTTP/1.1 200 Соединение установлено

‹X509Имя объекта '/C=US/ST=California/L=Mountain View/O=Google Inc/CN=mail.google.com'›

Он основан на pyOpenSSL, потому что мне также нужно было получить недействительные сертификаты, а встроенный модуль ssl Python всегда будет пытаться проверить сертификат, если он получен.

person kravietz    schedule 16.03.2012
comment
Это работает для вас, даже если вы подключаетесь к прокси-серверу HTTPS? В вашем примере вы подключаетесь к обычному прокси, который у меня тоже работает. Когда мне нужно дважды обернуть сокет, он терпит неудачу. - person Eli Courtwright; 16.03.2012
comment
Хороший ответ, но почему нельзя использовать ssl.wrap_socket? - person Dima Tisnek; 25.11.2013
comment
Это не работает в случае HTTPS-over-HTTPS и приводит к той же ошибке. - person 02strich; 26.11.2013
comment
Я получаю Error: [('SSL routines', 'SSL23_GET_SERVER_HELLO', 'unknown protocol')] и никакого бинарного мусора в выводе ssl depth-1. Я подозреваю, что вместо того, чтобы дважды оборачивать данные в SSL, OpenSSL повторно использует базовый сокет/fd и только один раз оборачивает данные. - person Dima Tisnek; 02.12.2013
comment
Я думаю, вы не используете HTTPS-прокси, это просто пример HTTPS в HTTP-прокси. - person Reorx; 22.03.2016

Судя по API библиотеки OpenSSL и GnuTLS, наложение SSLSocket на SSLSocket на самом деле невозможно напрямую, поскольку они предоставляют специальные функции чтения/записи для реализации шифрования, которые они не могут использовать сами при обертывании уже существующего SSLSocket. .

Таким образом, ошибка вызвана тем, что внутренний SSLSocket читает напрямую из системного сокета, а не из внешнего SSLSocket. Это заканчивается отправкой данных, не принадлежащих внешнему сеансу SSL, что заканчивается плохо и наверняка никогда не возвращает действительный ServerHello.

Делая вывод из этого, я бы сказал, что нет простого способа реализовать то, что вы (и, собственно, я) хотели бы достичь.

person 02strich    schedule 26.11.2013
comment
Звучит как разумное объяснение NPI. Возможно, вы знаете альтернативу? - person Dima Tisnek; 27.11.2013
comment
К сожалению, нет, если у вас есть какие-либо идеи, я весь внимание - person 02strich; 27.11.2013
comment
Я где-то зациклил данные обратно через socket.socketpair ;-) - person Dima Tisnek; 02.12.2013
comment
@qarma, так что вы читаете его из SSLSocket, записываете в пару сокетов, а затем снова читаете из второго SSLSocket на другом конце пары сокетов?! - person 02strich; 02.12.2013
comment
Да, это в основном так. Тем временем я обнаружил, что пакет twisted, похоже, поддерживает SSL-in-SSL через пользовательский BIO в своем модуле SSL/TLS, но это очень много зависимостей. - person Dima Tisnek; 03.12.2013

Наконец-то мне удалось расширить ответы @kravietz и @02strich.

Вот код

import threading
import select
import socket
import ssl

server = 'mail.google.com'
port = 443
PROXY = ("localhost", 4433)
CONNECT = "CONNECT %s:%s HTTP/1.0\r\nConnection: close\r\n\r\n" % (server, port)


class ForwardedSocket(threading.Thread):
    def __init__(self, s, **kwargs):
        threading.Thread.__init__(self)
        self.dest = s
        self.oursraw, self.theirsraw = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
        self.theirs = socket.socket(_sock=self.theirsraw)
        self.start()
        self.ours = ssl.wrap_socket(socket.socket(_sock=self.oursraw), **kwargs)

    def run(self):
        rl, wl, xl = select.select([self.dest, self.theirs], [], [], 1)
        print rl, wl, xl
        # FIXME write may block
        if self.theirs in rl:
            self.dest.send(self.theirs.recv(4096))
        if self.dest in rl:
            self.theirs.send(self.dest.recv(4096))

    def recv(self, *args):
        return self.ours.recv(*args)

    def send(self, *args):
        return self.outs.recv(*args)


def test():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect(PROXY)
    s = ssl.wrap_socket(s, ciphers="ALL:aNULL:eNULL")
    s.send(CONNECT)
    resp = s.read(4096)
    print (resp, )

    fs = ForwardedSocket(s, ciphers="ALL:aNULL:eNULL")
    fs.send("foobar")

Не против кастомного cihpers=, это только потому, что я не хотел иметь дело с сертификатами.

И есть вывод ssl depth-1, показывающий CONNECT, мой ответ на него ssagd и согласование ssl depth-2 и бинарный мусор:

[dima@bmg ~]$ openssl s_server  -nocert -cipher "ALL:aNULL:eNULL"
Using default temp DH parameters
Using default temp ECDH parameters
ACCEPT
-----BEGIN SSL SESSION PARAMETERS-----
MHUCAQECAgMDBALAGQQgmn6XfJt8ru+edj6BXljltJf43Sz6AmacYM/dSmrhgl4E
MOztEauhPoixCwS84DL29MD/OxuxuvG5tnkN59ikoqtfrnCKsk8Y9JtUU9zuaDFV
ZaEGAgRSnJ81ogQCAgEspAYEBAEAAAA=
-----END SSL SESSION PARAMETERS-----
Shared ciphers: [snipped]
CIPHER is AECDH-AES256-SHA
Secure Renegotiation IS supported
CONNECT mail.google.com:443 HTTP/1.0
Connection: close

sagq
�u\�0�,�(�$��
�"�!��kj98���� �m:��2�.�*�&���=5�����
��/�+�'�#��     ����g@32��ED���l4�F�1�-�)�%���</�A������
                                                        ��      ������
                                                                      �;��A��q�J&O��y�l
person Dima Tisnek    schedule 02.12.2013

Не похоже, что что-то не так с тем, что вы делаете; безусловно, можно вызвать wrap_socket() для существующего SSLSocket.

Ошибка «неизвестный протокол» может возникнуть (помимо других причин), если есть дополнительные данные, ожидающие чтения в сокете в точке, которую вы вызываете wrap_socket(), например, дополнительная \r\n или ошибка HTTP (из-за отсутствия сертификата на стороне сервера , например). Вы уверены, что прочитали все, что было доступно на тот момент?

Если вы можете заставить первый канал SSL использовать «простой» шифр RSA (т. е. не-Diffie-Hellman), вы можете использовать Wireshark для расшифровки потока, чтобы увидеть, что происходит.

person SimonJ    schedule 08.12.2010
comment
Я почти уверен, что в сокете нет ничего доступного, потому что, если я вызову sock.recv(1), он заблокируется на неопределенный срок. Тем не менее, спасибо за подтверждение того, что я могу дважды обернуть сокет. Я не могу изменить настройки SSL сервера, хотя я ценю предложение Wireshark — пожалуйста, дайте мне знать, если у вас есть другие идеи. - person Eli Courtwright; 09.12.2010
comment
Делайте то, что говорит SimonJ. 1) Сокеты SSL работают иначе, чем обычные сокеты. Даже если получены необработанные данные SSL, они не будут возвращены до тех пор, пока не будет получена полная и действительная запись SSL. 2) Вам не нужно ничего менять на сервере, чтобы принудительно использовать RSA, просто измените наборы шифров клиента, чтобы исключить все, что использует diffie-hellman. Конечно, вам также потребуется получить закрытый ключ сервера для расшифровки, поэтому, если вы не можете его получить, все, что вы можете увидеть, это шифр. Wireshark дает вам правдивую информацию: попробуйте. - person President James K. Polk; 09.12.2010
comment
Может ли клиент подключаться напрямую к серверу с помощью SSL? Возможно, топология вашей сети не позволяет этого, но было бы неплохо подтвердить, что нет несоответствия на уровне протокола (версия SSL или несовместимость набора шифров), которое препятствует обмену данными между конечными точками. - person SimonJ; 11.12.2010

Опираясь на ответ @kravietz. Вот версия, которая работает в Python3 через прокси Squid:

from OpenSSL import SSL
import socket

def verify_cb(conn, cert, errun, depth, ok):
        return True

server = 'mail.google.com'
port = 443
PROXY_ADDR = ("<proxy_server>", 3128)
CONNECT = "CONNECT %s:%s HTTP/1.0\r\nConnection: close\r\n\r\n" % (server, port)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(PROXY_ADDR)
s.send(str.encode(CONNECT))
s.recv(4096)

ctx = SSL.Context(SSL.SSLv23_METHOD)
ctx.set_verify(SSL.VERIFY_PEER, verify_cb)
ss = SSL.Connection(ctx, s)

ss.set_connect_state()
ss.do_handshake()
cert = ss.get_peer_certificate()
print(cert.get_subject())
ss.shutdown()
ss.close()

Это работает и в Python 2.

person Timothy C. Quinn    schedule 10.04.2019