使用 ssl 模块的 HTTPS 代理隧道
Posted
技术标签:
【中文标题】使用 ssl 模块的 HTTPS 代理隧道【英文标题】:HTTPS proxy tunneling with the ssl module 【发布时间】:2010-12-08 22:37:58 【问题描述】:我想手动(使用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>
这也是有道理的,因为我仍然需要与要通过隧道连接的example.com
服务器进行 SSL 握手。但是,如果不是立即发送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
那么如何与远程example.com
服务器进行 SSL 握手?
编辑:我很确定在第二次调用 wrap_socket
之前没有其他数据可用,因为调用 sock.recv(1)
会无限期阻塞。
【问题讨论】:
我的粗略猜测是ssl.wrap_socket
关心套接字连接状态。通常你会创建套接字,然后包装它,然后连接。在这里,您创建套接字,连接,然后包装。也许 ssl 只是被已经连接的底层套接字状态弄糊涂了。 github.com/kennethreitz/requests/blob/…
嘿,你运气好吗?我遇到了同样的问题,但也没有找到任何东西......
【参考方案1】:
如果 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 连接建立
它基于 pyOpenSSL,因为我也需要获取无效证书,而 Python 内置的 ssl 模块将始终尝试验证收到的证书。
【讨论】:
即使您连接到 HTTPS 代理,这对您有用吗?在您的示例中,您正在连接到一个常规代理,这也适用于我。当我需要双重包装失败的套接字时。 好答案,但为什么不能使用ssl.wrap_socket
?
这不适用于 HTTPS-over-HTTPS 的情况,并导致相同的错误
我得到 Error: [('SSL routines', 'SSL23_GET_SERVER_HELLO', 'unknown protocol')]
并且在 ssl depth-1 输出中没有二进制垃圾。我怀疑 OpenSSL 不是在 SSL 中包装数据两次,而是重用底层的 socket/fd 并且只包装一次数据。
我认为您没有使用 HTTPS 代理,它只是 HTTP 代理示例中的 HTTPS【参考方案2】:
从 OpenSSL 和 GnuTLS 库的 API 来看,将 SSLSocket 堆叠到 SSLSocket 上实际上是不可能的,因为它们提供了特殊的读/写函数来实现加密,而它们在包装 pre 时无法使用自己- 现有的 SSLSocket。
因此,该错误是由内部 SSLSocket 直接从系统套接字而不是从外部 SSLSocket 读取引起的。这以发送不属于外部 SSL 会话的数据而告终,结果很糟糕,并且肯定永远不会返回有效的 ServerHello。
由此得出结论,我想说没有简单的方法来实现您(实际上是我自己)想要完成的事情。
【讨论】:
听起来像是一个合理的解释NPI。你可能知道另一种选择吗? 很遗憾没有,如果你有任何想法,我全神贯注 我在某处通过socket.socketpair
循环返回数据;-)
@qarma 所以你从 SSLSocket 读取它,将其写入套接字对,然后从套接字对另一端的第二个 SSLSocket 再次读取?!
是的,基本上就是这样。同时我发现twisted
包似乎通过其 SSL/TLS 模块中的自定义BIO
支持 SSL-in-SSL,但这是很多依赖项。【参考方案3】:
最后我在@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=
,那只是因为我不想处理证书。
还有 depth-1 ssl 输出,显示 CONNECT
,我对它的回复 ssagd
和 depth-2 ssl 协商和二进制垃圾:
[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
【讨论】:
【参考方案4】:听起来你正在做的事情没有任何问题;当然可以在现有的SSLSocket
上调用wrap_socket()
。
如果在您调用 wrap_socket()
时在套接字上等待读取额外数据,例如额外的 \r\n
或 HTTP 错误(由于例如,服务器端缺少证书)。你确定你当时已经阅读了所有可用的东西吗?
如果您可以强制第一个 SSL 通道使用“普通”RSA 密码(即非 Diffie-Hellman),那么您可以使用 Wireshark 解密流以查看发生了什么。
【讨论】:
我很确定套接字上没有可用的东西,因为如果我调用sock.recv(1)
,那么它会无限期地阻塞。但是,感谢您确认我可以双重包装套接字。我无法更改服务器的 SSL 设置,但我很欣赏 Wireshark 的建议 - 如果您有任何其他想法,请告诉我。
按 SimonJ 说的做。 1) SSL 套接字的工作方式与常规套接字不同。即使收到了原始的 SSL 数据,在收到完整且有效的 SSL 记录之前,也不会返回任何数据。 2)您不需要在服务器上更改任何内容来强制 RSA,只需修改客户端密码套件以排除任何使用 diffie-hellman 的密码。当然,您还需要获取服务器的私钥来解密,所以如果您无法获得,那么您只能看到密码。 Wireshark 为您提供基本事实:试一试。
客户端可以使用SSL直接连接到服务器吗?可能是您的网络拓扑不允许这样做,但最好确认不存在阻止端点通信的协议级别不匹配(SSL 版本或密码套件不兼容)。【参考方案5】:
以@kravietz 的回答为基础。这是一个通过 Squid 代理在 Python3 中工作的版本:
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。
【讨论】:
以上是关于使用 ssl 模块的 HTTPS 代理隧道的主要内容,如果未能解决你的问题,请参考以下文章
如何设置 DataGrip 通过使用 DataGrip 的隧道以 SSL 模式连接 Cloud SQL
无法通过代理进行隧道传输。代理通过 https 返回“HTTP/1.1 407”