Прокси туннелирование HTTPS с модулем ssl - PullRequest
9 голосов
/ 09 декабря 2010

Я бы хотел вручную (используя модули socket и 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) блокирует бесконечно.*

Ответы [ 5 ]

7 голосов
/ 16 марта 2012

Это должно работать, если строка 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 Соединение установлено

<объект X509Name '/ C = США / ST = Калифорния / L = Маунтин-Вью / O = Google Inc / CN = mail.google.com'>

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

5 голосов
/ 27 ноября 2013

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

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

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

1 голос
/ 02 декабря 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-вывод глубины 1, показывающий CONNECT, мой ответ на него ssagd и ssl-согласование глубины-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
1 голос
/ 09 декабря 2010

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

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

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

0 голосов
/ 10 апреля 2019

Опираясь на @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.

...