爬虫经常要走代理,但代理“能看到什么”常被说错。这篇写一个会记录自己所见的最小代理,让同一个客户端分别请求 HTTP 和 HTTPS 地址,再看看代理认证、环境变量和 NO_PROXY 各自怎么起作用。
客户端得到:200 'hello from origin'
代理收到的第一行(注意是完整 URL,不是只有路径):GET http://plain.example.test:<端口>/secret/path?token=abc123 HTTP/1.1
代理收到的头:[('Host', 'plain.example.test:<端口>'), ('User-Agent', 'python-requests/2.34.2'), ('Accept-Encoding', 'gzip, deflate, zstd'), ('Accept', '*/*'), ('Connection', 'keep-alive'), ('Cookie', 'sid=SESSION42'), ('X-Api-Key', 'KEY99')]
源站收到的路径:/secret/path?token=abc123;源站收到的头:['Host', 'User-Agent', 'Accept-Encoding', 'Accept', 'Cookie', 'X-Api-Key', 'Connection']代理收到的请求行是完整 URL,头也是原样的。所以走明文 HTTP 经过一个代理,路径、查询串里的 token、Cookie 都对代理可见。
客户端得到:200 'hello from origin'
代理收到的第一行:CONNECT secure.example.test:<端口> HTTP/1.1
代理收到的头:[('Host', 'secure.example.test:<端口>')]
之后代理转发的字节:客户端→源站 1867 字节,源站→客户端 2511 字节
客户端发来的第一个字节 = 0x16(TLS 握手记录的类型是 0x16);代理能在其中看到主机名 secure.example.test(SNI 是明文):True
代理看到的字节里出现 '/secret/path':False
代理看到的字节里出现 'token=abc123':False
代理看到的字节里出现 'SESSION42':False
代理看到的字节里出现 'KEY99':False
源站(TLS 服务器)收到的路径:/secret/path?token=abc123HTTPS 通过 CONNECT host:port 让代理在两端之间转发字节,握手之后的内容在代理这边是密文。实测代理能看到的是目标主机和端口、以及 ClientHello 里明文的主机名(SNI),看不到路径、查询串、Cookie。这里的 TLS 终点是源站,代理不参与加密;中间人式的代理(会重新签发证书)不在本文范围内。
HTTP 地址,代理 URL 里没有账号:没有抛异常,r.status_code = 407
HTTPS 地址,代理 URL 里没有账号:ProxyError:onnect to proxy', OSError('Tunnel connection failed: 407 Proxy Authentication Required')))
HTTP 地址,代理 URL 带 alice:pw:200;代理收到 Proxy-Authorization = Basic YWxpY2U6cHc=
HTTPS 地址,代理 URL 带 alice:pw:200;CONNECT 请求里 Proxy-Authorization = Basic YWxpY2U6cHc=代理返回 407 时,requests 对 HTTP 地址只是把 407 当成普通响应返回(r.status_code == 407),对 HTTPS 地址则在建立隧道失败时抛 ProxyError。账号写在代理 URL 里(http://alice:pw@host:port),requests 会发 Proxy-Authorization: Basic …,HTTPS 时它在 CONNECT 请求里。这里的 Basic 只是 Base64 编码,不是加密。
设了 HTTP_PROXY:状态 200;经过代理的请求 1 个;源站一共收到 1 个请求(经代理转发的也算) 小写 http_proxy:状态 200;经过代理的请求 1 个;源站一共收到 1 个请求(经代理转发的也算) HTTP_PROXY + NO_PROXY=.example.test:状态 200;经过代理的请求 0 个;源站一共收到 1 个请求(经代理转发的也算) HTTP_PROXY + NO_PROXY=plain.example.test:状态 200;经过代理的请求 0 个;源站一共收到 1 个请求(经代理转发的也算) HTTP_PROXY + trust_env=False:状态 200;经过代理的请求 0 个;源站一共收到 1 个请求(经代理转发的也算) 只在代码里传 proxies=:经过代理 1 个 proxies= 传了代理,但目标是 127.0.0.1:经过代理 1 个(本进程 NO_PROXY=127.0.0.1,localhost)
环境变量 HTTP_PROXY(大小写都行)会被读取,NO_PROXY 能排除主机,trust_env=False 让 Session 不读环境变量。直接传 proxies= 则一定用,不看 NO_PROXY。排查“为什么请求被发到了代理 / 没发到代理”时,先看这几处。