Żądania Python HTTPS (urllib2) do niektórych stron kończą się niepowodzeniem na Ubuntu 12.04 bez proxy

23

Mam małą aplikację, którą napisałem w Pythonie i działała ... aż do wczoraj, kiedy to nagle zaczęło się pojawiać błąd w połączeniu HTTPS. Nie pamiętam, czy była aktualizacja, ale zarówno Python 2.7.3rc2, jak i Python 3.2 nie działają tak samo.

Przejrzałem go i dowiedziałem się, że dzieje się tak, gdy ludzie stoją za serwerem proxy, ale ja nie jestem (i nic się nie zmieniło w mojej sieci od czasu ostatniego działania). Komputer mojego systemu z systemem Windows i Python 2.7.2 nie ma problemów (w tej samej sieci).

>>> 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>

Co jest nie tak? Każda pomoc jest mile widziana.

PS .: Starsze wersje Pythona też nie działają, nie w moim systemie i nie w sesji na żywo z USB, ale działają w sesji na żywo Ubuntu 11.10.

Pablo
źródło
1
Czy dzieje się tak w przypadku każdej witryny SSL, z którą próbujesz się skontaktować, czy tylko tej? Jeśli nie występuje w przypadku każdej witryny, czy możesz nam powiedzieć, która strona jest przyczyną problemu?
James Henstridge
Cóż, sam nie jestem doświadczonym programistą i próbuję odczytać stronę z interfejsu API witryny, a to jedyne wywołanie, które wymaga protokołu SSL, więc nie wiem, czy robiłem to właściwie . Używałem go jak zwykłego wywołania urllib.urlopen (url) .read () i działało. Czy możesz podać adres innej witryny lub skrypt w języku Python, który odpowiedziałby na to pytanie?
Pablo
Och, zapomniałem wspomnieć: strona to Mediafire. Problemem jest wywołanie get_session_token.
Pablo
Udało mi się to odtworzyć na tej stronie. Zaktualizowałem twoje pytanie, aby uwzględnić daną witrynę. Podejrzewam, że jest to problem z OpenSSL, ponieważ wget również zawiedzie.
James Henstridge
Zdarza się to w przypadku stream.twitter.com w momencie pisania.
MarkR

Odpowiedzi:

15

Wydaje się, że jest to związane z dodaniem obsługi TLS 1.1 i 1.2 do wersji OpenSSL znalezionej w 12.04. Błąd połączenia można odtworzyć za pomocą narzędzia wiersza polecenia 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
---

Połączenie powiedzie się, jeśli wymuszę użycie TLS 1.0 z -tls1argumentem wiersza poleceń.

Proponuję tutaj zgłosić raport o błędzie dotyczący tego problemu:

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

James Henstridge
źródło
2
Dziękuję Ci! Zgłosiłem błąd. Sprawdź, czy możesz dodać do niego jakieś istotne informacje: bugs.launchpad.net/ubuntu/+source/openssl/+bug/965371
Pablo
1
W jaki sposób pomaga mu to obejść problem w Pythonie?
Cerin,
2
@Cerin: wyizolował problem jako błąd OpenSSL, a nie coś w Pythonie, i skierował go do korzystania z narzędzia do śledzenia błędów. Ten problem został już rozwiązany.
James Henstridge
12

Dla nowicjuszy pythonowych, takich jak ja, oto sposób na najprostszy sposób zastąpienia httplib. U góry skryptu Python dołącz następujące wiersze:


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

Odtąd możesz używać urllib lub cokolwiek innego, tak jak zwykle.

Uwaga: dotyczy Pythona 2.7. W przypadku rozwiązania w języku Python 3.x należy zastąpić klasę HTTPSConnection znalezioną w http.client. Zostawiam to jako ćwiczenie dla czytelnika. :-)

Jeff Mikels
źródło
2
Naprawdę podoba mi się to rozwiązanie, pozwala uniknąć modyfikacji bibliotek systemowych lub innych działań hakerskich.
MarkR
4
Nie działa przy użyciu Python 2.7.4 w systemie Ubuntu 12.04: Nazwa Błąd: nazwa „gniazdo” nie jest zdefiniowana. --- Musisz także dodać „gniazdo importu”.
Ben Walther
Działa świetnie na Ubuntu 13.04. Dzięki!
dharmatech
2
Nie ma powodu, aby łatać tylko httplib. Ludzie mogą korzystać z innych gniazd SSL. Można łataćssl zamiast tego jak w mojej odpowiedzi poniżej.
temoto
Daje mi to błądBadStatusLine: ''
Cerin,
8

Możesz uniknąć modyfikacji pliku httplib.py, modyfikując obiekt HTTPSConnection:

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)

Metoda żądania tworzy nowe gniazdo tylko wtedy, gdy nie zdefiniowano connection.sock. Utworzenie własnego przez dodanie parametru ssl_version spowoduje, że metoda żądania go wykorzysta. Wtedy wszystko inne działa jak zwykle.

Miałem ten sam problem i to działa dla mnie.

pozdrowienia

Adrikrun
źródło
7

Problem tkwi w tym ssl, że nie ma to nic wspólnego z HTTP, więc po co łatać, httplibjeśli można łatać ssl. Poniższy kod powinien naprawić wszystkie gniazda SSL, w tym między innymi HTTPS dla Python 2.6+ (wbudowane ssl, nie próbowałem pyopenssl).

import functools
import ssl

old_init = ssl.SSLSocket.__init__

@functools.wraps(old_init)
def ubuntu_openssl_bug_965371(self, *args, **kwargs):
  kwargs['ssl_version'] = ssl.PROTOCOL_TLSv1
  old_init(self, *args, **kwargs)

ssl.SSLSocket.__init__ = ubuntu_openssl_bug_965371
temoto
źródło
Dobra odpowiedź. Miły, elegancki sposób rozwiązania problemu.
chnrxn
3

EDYCJA httplib.py (/usr/lib/pythonX.X/httplib.py w systemie Linux)

ZNAJDŹ deklarację klasy HTTPSConnection

  class HTTPSConnection(HTTPConnection):
....

Kod klasy wewnątrz linii CHANGE

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

DO

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

Wtedy żądanie HTTP HTTPS powinno działać

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()
Yagger
źródło
3
Naprawdę nie jest poprawne edytowanie takiego pliku systemowego. Zamiast przedefiniować żadnych definicji, które muszą zostać zmienione, poprzez przekształcenie ich w swoim kodzie.
Przywróć Monikę - ζ--
2

Ten problem jest prawdopodobnie spowodowany wyłączeniem SSLv2 na serwerze WWW, ale Python 2.x domyślnie próbuje nawiązać połączenie z PROTOCOL_SSLv23.

Oto link do mojej odpowiedzi na podobny problem dotyczący przepełnienia stosu - /programming//a/24166498/41957

Aktualizacja: jest to funkcjonalnie to samo co powyższa odpowiedź @ temoto.

chnrxn
źródło
TypeError: niezwiązana metoda __init __ () musi zostać wywołana z instancją SSLSocket jako pierwszy argument (zamiast tego dostała instancję _socketobject)
sureshvv
Hmm, częściowe () nie działa dla metod klasowych. Wkrótce opublikuje lepsze rozwiązanie.
chnrxn
@sureshvv, jeśli możesz pomóc w sprawdzeniu rozwiązania, zostanie to docenione.
chnrxn
Odpowiedź @ temeto zadziałała.
sureshvv
1

Prostą poprawką, która działała dla mnie, było zastąpienie domyślnego protokołu SSL:

import ssl
ssl.PROTOCOL_SSLv23 = ssl.PROTOCOL_TLSv1
monis
źródło
Jest hackerski, ale działa raczej dobrze w dzisiejszym kontekście. Od czasu odkrycia podatności pudla TLSv1 stała się jedyną akceptowaną wersją w Internecie.
chnrxn