在Python中通过TCP代理连接发送SSL数据

1 投票
2 回答
5889 浏览
提问于 2025-04-17 18:05

我遇到了以下情况:
我必须使用一个HTTP代理来连接到一个HTTPS服务器。由于一些原因,我需要访问原始数据(也就是加密之前的数据),所以我选择使用socket库,而不是一些专门处理HTTP的库。
因此,我首先连接一个TCP socket到HTTP代理,并发送连接命令。

在这个时候,HTTP代理接受了连接,并似乎将所有后续的数据转发到目标服务器。
但是,如果我现在尝试切换到SSL,我会收到以下错误信息:

error:140770FC:SSL routines:SSL23_GET_SERVER_HELLO:unknown protocol

这表明socket尝试与HTTP代理进行握手,而不是与HTTPS目标服务器进行握手。

这是我目前写的代码:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  
s.connect(('proxy',9502))  
s.send("""CONNECT en.wikipedia.org:443 HTTP/1.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:15.0) Gecko/20100101 Firefox/15.0.1  
Proxy-Connection: keep-alive  
Host: en.wikipedia.org 

""")  

print s.recv(1000)  

ssl = socket.ssl(s, None, None)  
ssl.connect(("en.wikipedia.org",443))  

在连接到HTTP代理后,正确的方式是什么,以便打开一个SSL socket连接到目标服务器?

2 个回答

0

这个内容适用于Python 3。
< proxy > 是一个IP地址或者域名。
< port > 是代理服务器监听的端口,可能是443、80,或者其他你设置的端口。
< endpoint > 是你想通过代理连接的最终服务器。
< cn > 是一个可选的SNI字段,最终服务器可能会需要这个字段。

import socket,ssl

def getcert_sni_proxy(cn,endpoint,PROXY_ADDR=("<proxy>", <port>)):
    #prepare the connect phrase
    CONNECT = "CONNECT %s:%s HTTP/1.0\r\nConnection: close\r\n\r\n" % (endpoint, 443)

    #connect to the actual proxy
    conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    conn.connect(PROXY_ADDR)
    conn.send(str.encode(CONNECT))
    conn.recv(4096)
    
    #set the cipher for the ssl layer
    context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)

    #connect to the final endpoint via the proxy, sending an optional servername information [cn here]
    sock = context.wrap_socket(conn, server_hostname=cn)
    #retreive certificate from the server
    certificate = ssl.DER_cert_to_PEM_cert(sock.getpeercert(True))
    return certificate
1

(一般来说,使用现成的HTTPS库,比如PyCurl,会比自己实现要简单得多。)

首先,别把你的变量命名为 ssl。这个名字已经被 ssl 模块用了,你不想把它给覆盖掉。

其次,别再第二次使用 connect。你已经连接上了,现在需要做的是包装这个套接字。因为Python默认不进行证书验证,所以你需要验证远程证书和主机名。

下面是具体步骤:

  • 先建立一个明文连接,像前面几行那样使用 CONNECT
  • 读取你收到的HTTP响应,确保你得到的是200状态码。(你需要逐行读取头部信息。)
  • 使用 ssl_s = ssl.wrap_socket(s, cert_reqs=ssl.CERT_REQUIRED, ssl_version=ssl.PROTOCOL_TLS1, ca_certs='/path/to/cabundle.pem') 来包装这个套接字。然后,验证主机名。值得一读的是 这个回答:关于 connect 方法以及它在包装套接字后做了什么。
  • 最后,像使用普通套接字一样使用 ssl_s。不要再调用 connect 了。

撰写回答