Python urllib2, podstawowe uwierzytelnianie HTTP i tr.im

84

Bawię się, próbując napisać kod, aby użyć interfejsów API tr.im do skrócenia adresu URL.

Po przeczytaniu http://docs.python.org/library/urllib2.html próbowałem:

   TRIM_API_URL = 'http://api.tr.im/api'
   auth_handler = urllib2.HTTPBasicAuthHandler()
   auth_handler.add_password(realm='tr.im',
                             uri=TRIM_API_URL,
                             user=USERNAME,
                             passwd=PASSWORD)
   opener = urllib2.build_opener(auth_handler)
   urllib2.install_opener(opener)
   response = urllib2.urlopen('%s/trim_simple?url=%s'
                              % (TRIM_API_URL, url_to_trim))
   url = response.read().strip()

response.code to 200 (myślę, że powinno być 202). adres url jest prawidłowy, ale wydaje się, że podstawowe uwierzytelnianie HTTP nie zadziałało, ponieważ skróconego adresu URL nie ma na mojej liście adresów URL (pod adresem http://tr.im/?page=1 ).

Po przeczytaniu http://www.voidspace.org.uk/python/articles/authentication.shtml#doing-it-properly spróbowałem też:

   TRIM_API_URL = 'api.tr.im/api'
   password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
   password_mgr.add_password(None, TRIM_API_URL, USERNAME, PASSWORD)
   auth_handler = urllib2.HTTPBasicAuthHandler(password_mgr)
   opener = urllib2.build_opener(auth_handler)
   urllib2.install_opener(opener)
   response = urllib2.urlopen('http://%s/trim_simple?url=%s'
                              % (TRIM_API_URL, url_to_trim))
   url = response.read().strip()

Ale mam takie same wyniki. (kod odpowiedzi to 200, a adres URL jest prawidłowy, ale nie został zapisany na moim koncie pod adresem http://tr.im/ .)

Jeśli używam parametrów ciągu zapytania zamiast podstawowego uwierzytelniania HTTP, na przykład:

   TRIM_API_URL = 'http://api.tr.im/api'
   response = urllib2.urlopen('%s/trim_simple?url=%s&username=%s&password=%s'
                              % (TRIM_API_URL,
                                 url_to_trim,
                                 USERNAME,
                                 PASSWORD))
   url = response.read().strip()

... to nie tylko adres URL jest prawidłowy, ale jest zapisywany na moim koncie tr.im. (Chociaż kod odpowiedzi to nadal 200.)

Jednak musi być coś nie tak z moim kodem (a nie z API tr.im), ponieważ

$ curl -u yacitus:xxxx http://api.tr.im/api/trim_url.json?url=http://www.google.co.uk

...zwroty:

{"trimpath":"hfhb","reference":"nH45bftZDWOX0QpVojeDbOvPDnaRaJ","trimmed":"11\/03\/2009","destination":"http:\/\/www.google.co.uk\/","trim_path":"hfhb","domain":"google.co.uk","url":"http:\/\/tr.im\/hfhb","visits":0,"status":{"result":"OK","code":"200","message":"tr.im URL Added."},"date_time":"2009-03-11T10:15:35-04:00"}

... a adres URL pojawia się na mojej liście adresów URL pod adresem http://tr.im/?page=1 .

A jeśli biegnę:

$ curl -u yacitus:xxxx http://api.tr.im/api/trim_url.json?url=http://www.google.co.uk

... znowu otrzymuję:

{"trimpath":"hfhb","reference":"nH45bftZDWOX0QpVojeDbOvPDnaRaJ","trimmed":"11\/03\/2009","destination":"http:\/\/www.google.co.uk\/","trim_path":"hfhb","domain":"google.co.uk","url":"http:\/\/tr.im\/hfhb","visits":0,"status":{"result":"OK","code":"201","message":"tr.im URL Already Created [yacitus]."},"date_time":"2009-03-11T10:15:35-04:00"}

Kod notatki to 201, a wiadomość to „Tr.im URL Already Created [yacitus]”.

Nie mogę poprawnie wykonywać podstawowego uwierzytelniania HTTP (w żadnej z prób). Czy widzisz mój problem? Może powinienem spojrzeć i zobaczyć, co jest przesyłane przez drut? Nigdy wcześniej tego nie robiłem. Czy istnieją interfejsy API Pythona, których mogę używać (być może w pdb)? A może jest inne narzędzie (najlepiej dla Mac OS X), którego mogę użyć?

Daryl Spitzer
źródło
2
witryna musi zwrócić "WWW-Authenticate"i kod 401, zanim urllib2 (lub httplib2) wyśle ​​Twoje poświadczenia. Zobacz moją odpowiedź poniżej .
Mark Mikofski
Uwaga: ta usługa wydaje się nie działać.
Laurel

Odpowiedzi:

246

Wydaje się, że działa naprawdę dobrze (zaczerpnięte z innego wątku)

import urllib2, base64

request = urllib2.Request("http://api.foursquare.com/v1/user")
base64string = base64.encodestring('%s:%s' % (username, password)).replace('\n', '')
request.add_header("Authorization", "Basic %s" % base64string)   
result = urllib2.urlopen(request)
Ben Keating
źródło
7
Zamiast base64.encodestring i zamień, użyj base64.standard_b64encode
Paweł Polewicz
5
request.add_header('Authorization', b'Basic ' + base64.b64encode(username + b':' + password))
jfs
1
Na podstawie tej odpowiedzi utworzyłem pakiet urllib2_prior_auth, który nie ma żadnych zależności poza stdlib i próbuję wprowadzić odpowiednią zmianę do standardowej biblioteki .
mcepl
5
Lub jeszcze krócej / unikanie importu: request.add_header ('Authorization', b'Basic '+ (nazwa użytkownika + b': '+ hasło) .encode (' base64 '))
makapuf
20

Naprawdę tanie rozwiązanie:

urllib.urlopen('http://user:[email protected]/api')

(co możesz zdecydować, że nie jest odpowiednie z wielu powodów, takich jak bezpieczeństwo adresu URL)

Przykład Github API :

>>> import urllib, json
>>> result = urllib.urlopen('https://personal-access-token:[email protected]/repos/:owner/:repo')
>>> r = json.load(result.fp)
>>> result.close()
Ali Afshar
źródło
Czy są jakieś zalety w porównaniu z używaniem parametrów ciągu zapytania?
Daryl Spitzer
1
Daryl: Jeśli to zadziała, powiedziałbym, że jest to zaleta, tak i prawdopodobnie bezpieczniejsza niż argumenty będące ciągami zapytań, ponieważ większość klientów http jest nieco bardziej ostrożna, jeśli chodzi o ich obsługę.
Ali Afshar
Prawdopodobnie pójdę z tym (więc dostaniesz moją opinię), ale nadal chciałbym dowiedzieć się, co jest nie tak z moim kodem (więc nie będzie to moja akceptowana odpowiedź).
Daryl Spitzer
36
To zwraca błąd ... InvalidURL: nonnumeric port: '[email protected]/api'
Nick Bolton
5
@nbolton, upewnij się, że nie używasz urllib2.urlopen (url)
CantGetANick
13

Spójrz na tę odpowiedź na SO, a także na ten samouczek dotyczący podstawowego uwierzytelniania z brakującego podręcznika urllib2 .

Aby podstawowe uwierzytelnianie urllib2 działało, odpowiedź http musi zawierać kod HTTP 401 Unauthorized i klucz "WWW-Authenticate"z wartością, w "Basic"przeciwnym razie Python nie wyśle ​​twoich danych logowania i będziesz musiał albo użyć żądań , albo urllib.urlopen(url)z loginem w URL lub dodać nagłówek jak w @ Flowpoke za odpowiedź .

Możesz zobaczyć swój błąd, umieszczając swój urlopenw bloku próbnym:

try:
    urllib2.urlopen(urllib2.Request(url))
except urllib2.HTTPError, e:
    print e.headers
    print e.headers.has_key('WWW-Authenticate')
Mark Mikofski
źródło
Pomogło mi to, ponieważ wydrukowanie nagłówków doprowadziło mnie do zorientowania się, że pomyliłem się w dziedzinie uwierzytelniania. +1
wolna przestrzeń
7

Zalecanym sposobem jest użycie requestsmodułu :

#!/usr/bin/env python
import requests # $ python -m pip install requests
####from pip._vendor import requests # bundled with python

url = 'https://httpbin.org/hidden-basic-auth/user/passwd'
user, password = 'user', 'passwd'

r = requests.get(url, auth=(user, password)) # send auth unconditionally
r.raise_for_status() # raise an exception if the authentication fails

Oto urllib2wariant oparty na jednym źródle Python 2/3 zgodny z :

#!/usr/bin/env python
import base64
try:
    from urllib.request import Request, urlopen
except ImportError: # Python 2
    from urllib2 import Request, urlopen

credentials = '{user}:{password}'.format(**vars()).encode()
urlopen(Request(url, headers={'Authorization': # send auth unconditionally
    b'Basic ' + base64.b64encode(credentials)})).close()

Python 3.5+ wprowadza,HTTPPasswordMgrWithPriorAuth() który umożliwia:

.. wyeliminować niepotrzebną obsługę odpowiedzi 401 lub bezwarunkowo wysyłać poświadczenia przy pierwszym żądaniu w celu komunikacji z serwerami, które zwracają odpowiedź 404 zamiast 401, jeśli nagłówek Authorization nie zostanie wysłany.

#!/usr/bin/env python3
import urllib.request as urllib2

password_manager = urllib2.HTTPPasswordMgrWithPriorAuth()
password_manager.add_password(None, url, user, password,
                              is_authenticated=True) # to handle 404 variant
auth_manager = urllib2.HTTPBasicAuthHandler(password_manager)
opener = urllib2.build_opener(auth_manager)

opener.open(url).close()

Jest to łatwe do wymiany HTTPBasicAuthHandler()w ProxyBasicAuthHandler()razie potrzeby w tym przypadku.

jfs
źródło
4

Sugerowałbym, że obecnym rozwiązaniem jest użycie mojego pakietu urllib2_prior_auth, który całkiem ładnie to rozwiązuje (pracuję nad włączeniem do standardowego lib.

mcepl
źródło
1
Został dołączony do Pythona 3.5 asurrlib.request.HTTPBasicPriorAuthHandler
mcepl
3

Stosowane są te same rozwiązania, co Python urllib2 Basic Auth Problem .

zobacz https://stackoverflow.com/a/24048852/1733117 ; możesz podklasę, urllib2.HTTPBasicAuthHandleraby dodać Authorizationnagłówek do każdego żądania, które pasuje do znanego adresu URL.

class PreemptiveBasicAuthHandler(urllib2.HTTPBasicAuthHandler):
    '''Preemptive basic auth.

    Instead of waiting for a 403 to then retry with the credentials,
    send the credentials if the url is handled by the password manager.
    Note: please use realm=None when calling add_password.'''
    def http_request(self, req):
        url = req.get_full_url()
        realm = None
        # this is very similar to the code from retry_http_basic_auth()
        # but returns a request object.
        user, pw = self.passwd.find_user_password(realm, url)
        if pw:
            raw = "%s:%s" % (user, pw)
            auth = 'Basic %s' % base64.b64encode(raw).strip()
            req.add_unredirected_header(self.auth_header, auth)
        return req

    https_request = http_request
dnozay
źródło
Czy wezwanie nie jest stripzbędne po b64encode?
Mihai Todor