有一位朋友在留言中问了下面的问题:
> cfw中启用Specify Protocol或者pac对访问https网站依旧无效
于是我又打开阔别已久的Windows开发机看了一下(顺便一提,果子也挺烂的,尤其是ARM版的,不过看在字体渲染的份上还是先用果子吧):
问题在于Clash for Windows的Specify Protocol处理其实还是有问题,如果你之前没有仔细完全阅读下面那个设定代理服务器的函数,翻下去看一下它,就在倒数第二张图。
可以看到如果只是单纯地把注册表项的内容由
127.0.0.1:7890
改为http://127.0.0.1:7890
的话,urllib只会返回一个只有一个key也就是http
的代理dict。
这时候从pip的请求调用链往上找,可以看到负责决定使用这个dict中哪个代理的代码是 requests/utils.py
的select_proxy
函数:因为红框中的部分的限制,当你请求
https://pypi.org
的时候,只有key为https
/https://pypi.org
/all
/all://pypi.org
的代理会被使用,上面那个http
的代理自然也就不会被使用。
BTW,因为这个代码是requests库的,这也就意味着在Windows平台上Clash for Windows的系统代理不会影响到大部分py应用的http请求。
而正确的做法是什么呢?让我们在IE中设置上代理,然后看一看它给出的行为:同时,因为urllib中还存在代理的类型推测代码,所以正确的设置应该是:
http=http://127.0.0.1:7890;https=http://127.0.0.1:7891
21/06/07 Update: CFW 解决了 这个问题,此问题同样存在于 QV2ray
22/02/05:似乎此问题在新版Python中的报错信息会变成
There was a problem confirming the ssl certificate: HTTPSConnection
Pool(host='pypi.org', port=443): Max retries exceeded with url: /simple/plotly/ (Caused by SSLError(SSLEOFError(8, 'EOF
occurred in violation of protocol (_ssl.c:1123)')))
或者类似的错误。
在某书的文章中我看到了类似的问题,报错是 ValueError: check_hostname requires server_hostname
,虽然我没复现,不知道对应的版本,也没看过对应的代码,不过就逻辑来说应当是相同的问题。最近终于想写点Python,结果打开PyCharm开个venv之后,pip给我疯狂报错:
WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError(0, 'Error'))': /simple /gitpython/ WARNING: Retrying (Retry(total=3, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError(0, 'Error'))': /simple /gitpython/
偏偏在venv外面就是一切正常,这促使我想找到问题。
遇到这种问题那肯定是直接debug走起,首先用文本搜索到抛这个错的函数,然后在嫌疑语句上都打上断,我们就能找到罪魁祸首:
追到这个函数的实现里面,虽然直接看起来没什么问题,但是和venv外没问题的老版pip一比较,很容易就能发现不对劲:选中的那两行在老版是不存在的
有了这个额外信息,我们很容易就能找到这个 Support for web proxies is broken in pip 20.3 · Issue #9190 · pypa/pip (github.com) 来说明问题,2016年底,curl加入了这个把https协议前缀另加解释与定义的联盟:HTTPS proxy with curl | daniel.haxx.se,而urllib3显然也跟上了这个脚步:
在因为前缀设置了
tls_in_tls_required
之后,urllib3会企图把这个代理服务器看作一个套了tls的http CONNECT代理。
但是问题是,我并没有设置 https://
前缀的代理服务器,这个行为是什么神奇的情况呢?
继续向下追,找到如何获取代理的:确实,env里面没有proxy,那按照windows的习俗找找注册表也情有可原对吧?
这个函数的内部实现是这样:
看红框的行为是不是好像很眼熟?IE的代理设置似乎就是这样的?
不,并不是一样的,因为IE的代理设置把HTTPS(在
zh-MS
方言里叫安全)代理定义为支持 CONNECT
动词的HTTP代理,尽管很久以来人们都是这样用的,但是当它前面出现一个协议前缀的时候就不一样了。
因为clash for windows打开系统代理的代理配置看起来并没有写明了protocol:所以首先,我们的py会根据IE时代的约定俗成把这样一个没有指明protocol的proxy url自动补全三种协议,然后再按照约定俗成的行为为https请求使用https_proxy,最后在一个http代理上试图开tls。
这个配置在IE时代行为会是正常的,在现代的库中行为也是正常的,但是对于这样一个混杂了两种行为的库,模糊不清就成了问题。
上面的截图并不是urllib3的,而是 属于py自己的标准库urllib的,也算是urllib3开发人员的思想和urllib的历史遗留实现冲突了吧。