在没有代理的 Ubuntu 12.04 上,对某些网站的 Python HTTPS 请求(urllib2)会失败

在没有代理的 Ubuntu 12.04 上,对某些网站的 Python HTTPS 请求(urllib2)会失败

我用 Python 编写了一个小应用程序,以前它可以正常工作……直到昨天,它突然开始在 HTTPS 连接中出现错误。我不记得是否有更新,但 Python 2.7.3rc2 和 Python 3.2 都同样失败了。

我谷歌了一下,发现这种情况发生在人们使用代理时,但我没有(而且自从上次使用以来我的网络没有任何变化)。我的系统计算机运行 Windows 和 Python 2.7.2 没有任何问题(在同一个网络中)。

>>> url = 'https://www.mediafire.com/api/user/get_session_token.php'
>>> response = urllib2.urlopen(url).read()
  File "/usr/lib/python2.7/urllib2.py", line 126, in urlopen
    return _opener.open(url, data, timeout)
  File "/usr/lib/python2.7/urllib2.py", line 400, in open
    response = self._open(req, data)
  File "/usr/lib/python2.7/urllib2.py", line 418, in _open
    '_open', req)
  File "/usr/lib/python2.7/urllib2.py", line 378, in _call_chain
    result = func(*args)
  File "/usr/lib/python2.7/urllib2.py", line 1215, in https_open
    return self.do_open(httplib.HTTPSConnection, req)
  File "/usr/lib/python2.7/urllib2.py", line 1177, in do_open
    raise URLError(err)
urllib2.URLError: <urlopen error [Errno 8] _ssl.c:504: EOF occurred in violation of protocol>

出了什么问题?如能得到任何帮助,我们将不胜感激。

附言:旧版本的 Python 也不起作用,在我的系统中不起作用,在 USB 的实时会话中也不起作用,但在 Ubuntu 11.10 实时会话中可以起作用。

答案1

这似乎与 12.04 版 OpenSSL 中增加了对 TLS 1.1 和 1.2 的支持有关。可以使用 OpenSSL 命令行工具重现连接失败:

$ openssl s_client -connect www.mediafire.com:443
CONNECTED(00000003)
140491065808544:error:140790E5:SSL routines:SSL23_WRITE:ssl handshake failure:s23_lib.c:177:
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 0 bytes and written 320 bytes
---
New, (NONE), Cipher is (NONE)
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
---

-tls1如果我使用命令行参数强制连接使用 TLS 1.0,则连接成功。

我建议您在此处提交有关此问题的错误报告:

https://bugs.launchpad.net/ubuntu/+filebug

答案2

对于像我这样的 Python 新手来说,这是覆盖 httplib 的最简单方法。在 Python 脚本的顶部,包含以下几行:


import httplib
from httplib import HTTPConnection, HTTPS_PORT
import ssl

class HTTPSConnection(HTTPConnection):
    "This class allows communication via SSL."
    default_port = HTTPS_PORT

    def __init__(self, host, port=None, key_file=None, cert_file=None,
            strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
            source_address=None):
        HTTPConnection.__init__(self, host, port, strict, timeout,
                source_address)
        self.key_file = key_file
        self.cert_file = cert_file

    def connect(self):
        "Connect to a host on a given (SSL) port."
        sock = socket.create_connection((self.host, self.port),
                self.timeout, self.source_address)
        if self._tunnel_host:
            self.sock = sock
            self._tunnel()
        # this is the only line we modified from the httplib.py file
        # we added the ssl_version variable
        self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version=ssl.PROTOCOL_TLSv1)

#now we override the one in httplib
httplib.HTTPSConnection = HTTPSConnection
# ssl_version corrections are done

从现在开始,您可以像平常一样使用 urllib 或任何您使用的东西。

注意:这是针对 Python 2.7 的。对于 Python 3.x 解决方案,您需要覆盖 http.client 中的 HTTPSConnection 类。我将其留给读者练习。:-)

答案3

您可以通过修改 HTTPSConnection 对象来避免修改 httplib.py 文件:

import httplib, ssl, socket

conn = httplib.HTTPSConnection(URL.hostname)
sock = socket.create_connection((conn.host, conn.port), conn.timeout, conn.source_address)
conn.sock = ssl.wrap_socket(sock, conn.key_file, conn.cert_file, ssl_version=ssl.PROTOCOL_TLSv1)
conn.request('POST', URL.path + URL.query)

仅当 connection.sock 未定义时,请求方法才会创建新套接字。创建自己的套接字并添加 ssl_version 参数将使请求方法使用它。然后其他一切都照常进行。

我遇到了同样的问题,这对我有用。

问候

答案4

编辑 httplib.py (Linux 上为 /usr/lib/pythonX.X/httplib.py)

查找 HTTPSConnection 类声明

  class HTTPSConnection(HTTPConnection):
....

类内代码 CHANGE 行

self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file)

self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version=ssl.PROTOCOL_TLSv1)

然后 httplib HTTPS 请求应该可以工作

import httplib
from urlparse import urlparse
url = XXX
URL = urlparse(url)
connection = httplib.HTTPSConnection(URL.hostname)
connection.request('POST', URL.path + URL.query)
response = connection.getresponse()

相关内容