【问题标题】:Requests Library Force Use of HTTP/1.1 On HTTPS Proxy CONNECT请求库强制在 HTTPS 代理 CONNECT 上使用 HTTP/1.1
【发布时间】:2017-04-12 22:50:55
【问题描述】:

我遇到了一个行为异常的 HTTP 代理服务器的问题。不幸的是,我无法控制代理服务器——它是 IBM 的“企业”产品。代理服务器是用于软件测试的服务虚拟化解决方案的一部分。

根本问题(我认为*)是代理服务器发回 HTTP/1.0 响应。我可以从 SOAP UI(Java 应用程序)和命令行 curl 让它正常工作,但 Python 拒绝连接。据我所知,Python 的行为是正确的,而其他两个则不是,因为服务器期望 HTTP/1.1 响应(它至少希望主机标头将服务请求路由到给定的存根)。

有没有办法获取请求,或者底层的 urllib3,或者更远的 http lib 以始终使用 http1.1,即使另一端似乎正在使用 1.0?

这是一个示例程序(不幸的是,它需要您安装带有 RTCP 的 IBM Ration Integration Tester 才能真正复制)来重现该问题:

import http.client as http_client
http_client.HTTPConnection.debuglevel = 1
import logging
import requests
logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)
requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True

requests.post("https://host:8443/axl", 
            headers={"soapAction": '"CUCM:DB ver=9.1 updateSipTrunk"'}, 
            data='<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tns="http://www.cisco.com/AXL/API/9.1"><soapenv:Header/><soapenv:Body><tns:updateSipTrunk><name>PLACEHOLDER</name><newName>PLACEHOLDER</newName><destinations><destination><addressIpv4>10.10.1.5</addressIpv4><sortOrder>1</sortOrder></destination></destinations></tns:updateSipTrunk></soapenv:Body></soapenv:Envelope>', 
            verify=False)

(代理通过 HTTPS_PROXY 环境变量配置)

错误前的调试输出,注意HTTP/1.0:

INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): host.com
send: b'CONNECT host.com:8443 HTTP/1.0\r\n'
send: b'\r\n'
header: Host: host.com:8443

header: Proxy-agent: Green Hat HTTPS Proxy/1.0

在 RHEL 6 中出现的确切错误文本是:

requests.exceptions.SSLError: [SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:646)

即使此处显示了主机标头,它也不会显示在网络上。我通过 tcpdump 确认了这一点:

14:03:14.315049 IP sourcehost.53214 > desthost.com: Flags [P.], seq 0:32, ack 1, win 115, options [nop,nop,TS val 2743933964 ecr 4116114841], length 32
        0x0000:  0000 0c07 ac00 0050 56b5 4044 0800 4500  .......PV.@D..E.
        0x0010:  0054 3404 4000 4006 2ca0 0af8 3f15 0afb  .T4.@.@.,...?...
        0x0020:  84f8 cfde 0c7f a4f8 280a 4ebd b425 8018  ........(.N..%..
        0x0030:  0073 da46 0000 0101 080a a38d 1c0c f556  .s.F...........V
        0x0040:  XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX  ..CONNECT.host
        0x0050:  XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX  xx:8443.HTTP/1.0
        0x0060:  0d0a                          

当我用详细卷曲它时,输出是这样的:

* About to connect() to proxy proxy-host.com port 3199 (#0)
*   Trying 10.**.**.** ... connected
* Connected to proxy-host.com (10.**.**.**) port 3199 (#0)
* Establish HTTP proxy tunnel to host.com:8443
> CONNECT host.com:8443 HTTP/1.1
> Host: host.com:8443
> User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.19.1 Basic ECC zlib/1.2.3 libidn/1.18 libssh2/1.4.2
> Proxy-Connection: Keep-Alive
> soapAction: "CUCM:DB ver=9.1 updateSipTrunk"
>
< HTTP/1.0 200 OK
< Host: host.com:8443
< Proxy-agent: Green Hat HTTPS Proxy/1.0
<
* Proxy replied OK to CONNECT request
* Initializing NSS with certpath: sql:/etc/pki/nssdb
*   CAfile: /path/to/store/ca-bundle.crt
  CApath: none
* SSL connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256

在此之后被截断。连接后,您可以看到来自代理的 HTTP/1.0 响应。 curl 的 tcpdump 也清楚地显示了主机头,以及 HTTP 1.1。

*我不能完全确定这是根本问题,因为我无法测试它。我确实看到了 HTTP/1.0 响应,并且可以看出我的非工作 Python 代码发送 CONNECT HTTP/1.0 消息,而工作 Java 发送 HTTP/1.1 消息,Curl 也是如此。问题可能是不相关的(尽管我发现这不太可能)或者 Python 行为不端,而不是 Java/curl。我只是没有足够的知识来确定。

那么,有没有办法强制 urllib3/requests 始终使用 HTTP v1.1?

【问题讨论】:

  • 看起来它正在接收主机头。看起来代理连接很好,但之后由于某种原因导致 SSL 协商失败。
  • 是什么让你说它接收主机头?它不是线路上的 CONNECT 消息的一部分。它是 curl 的 CONNECT 消息的一部分(我没有在此处发布 curl tcpdump)
  • 我认为对 CONNECT 的 HTTP/1.0 响应很好,不会导致问题。它从根本上表明它建立了隧道,并且之后的任何内容都通过代理不透明地传递。正如其他人所说,报告的错误表明存在 TLS 握手问题。
  • 您正在从代理服务器接收主机标头,而不是发送它,它看起来不是必需的,因为 CONNECT 的所有信息都在 URL 中)。至少,这就是我将日志解释为:打印它收到的主机和代理代理头。此时,它必须协商 TLS,然后发送一组全新的标头。
  • 我也追了很久的TLS路线。天,其实。我更新了 python,检查了 OpenSSL 版本,在互联网上搜索了 openSSL 和 JSSE 之间的不兼容性......什么也没找到。我连接到其他任何东西都没有问题,只是这个。我并不是说它不可能是 SSL 问题……但这是成功的 Java 客户端和失败的 Python 客户端之间流量的唯一显着差异。不过,如果您有特定的想法,我很乐意尝试。

标签: python curl python-requests python-3.5 http-proxy


【解决方案1】:

httplib (which requests relies upon for HTTP(S) heavy lifting) 总是将HTTP/1.0CONNECT 一起使用:

Lib/httplib.py:788:

def _tunnel(self):
    self.send("CONNECT %s:%d HTTP/1.0\r\n" % (self._tunnel_host,
        self._tunnel_port))
    for header, value in self._tunnel_headers.iteritems():
        self.send("%s: %s\r\n" % (header, value))
    self.send("\r\n")
    <...>

因此,除了编辑子例程之外,您不能“强制”它在此处使用“HTTP/1.1”。


如果代理不支持 HTTP/1.0,这可能是问题 - 特别是,1.0 不需要 Host: 标头,实际上,正如您通过将日志输出与上面的代码进行比较可以看到的那样,@ 987654331@ 不发送。 While, in verity, a proxy may expect it regardless。但如果是这种情况,您应该从代理收到错误或响应 CONNECT 的其他内容 - 除非代理非常糟糕以至于它用一些默认值(或垃圾)替换了 Host:,无论如何都会返回 200并尝试连接上帝知道在哪里,此时您会超时。

您可以通过将_tunnel_headers(间接)添加到Host: 标头来使httplib 添加到CONNECT:

s=requests.Session()
proxy_url=os.environ['HTTPS_PROXY']
s.proxies["https"]=proxy_url
# have to specify proxy here because env variable is only detected by httplib code
#while we need to trigger requests' proxy logic that acts earlier
# "https" means any https host. Since a Session persists cookies,
#it's meaningless to make requests to multiple hosts through it anyway.

pm=s.get_adapter("https://").proxy_manager_for(proxy_url)
pm.proxy_headers['Host']="host.com"
del pm,proxy_url
<...>
s.get('https://host.com')

【讨论】:

  • 感谢您的建议。我尝试在那里添加标题,但我仍然没有看到它通过电线。我将剥离请求并直接访问源 httplib... 这可能比它的价值更多,但我需要深入了解。
  • 从头开始,我确实看到了标题,但在下一个数据包中。在 curl 中,它在同一个数据包中。这将比我想象的更难调查......
  • @Keozon 这是 Python,一种解释性语言,看在上帝的份上!只需单步执行pdb 中的代码并找出出错的地方。
  • 这最终是用户错误和代理服务器上的 dns 配置的组合。 Python 脚本调用的是主机名,而不是 FQDN,它在连接后无法在代理上查找。它被忽视了很长时间,因为我可以很容易地直接连接,通过不同的代理,通过 HTTP。 Curl 恰好是完全合格的,否则它也会失败。谢谢你的帮助!标记为已接受,因为您准确地回答了直接问题,并给出了下一步的建议。
【解决方案2】:

如果您不依赖 requests 库,您可能会发现以下 sn-p 很有用:

import http.client

conn = http.client.HTTPSConnection("proxy.domain.lu", 8080)
conn.set_tunnel("www.domain.org", 443, headers={'User-Agent': 'curl/7.56.0'})
conn.request("GET", "/api")
response = conn.getresponse()

print( response.read() )

【讨论】:

    猜你喜欢
    • 2016-07-06
    • 2017-08-17
    • 2015-02-25
    • 2022-08-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-27
    • 2014-05-17
    相关资源
    最近更新 更多