Python urllib2, autenticazione HTTP di base e tr.im


85

Sto giocando, cercando di scrivere del codice per utilizzare le API tr.im per abbreviare un URL.

Dopo aver letto http://docs.python.org/library/urllib2.html , ho provato:

   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 è 200 (penso che dovrebbe essere 202). url è valido, ma l'autenticazione HTTP di base non sembra aver funzionato, perché l'URL abbreviato non è nel mio elenco di URL (su http://tr.im/?page=1 ).

Dopo aver letto http://www.voidspace.org.uk/python/articles/authentication.shtml#doing-it-properly ho anche provato:

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

Ma ottengo gli stessi risultati. (response.code è 200 e l'URL è valido, ma non è registrato nel mio account su http://tr.im/ .)

Se utilizzo i parametri della stringa di query invece dell'autenticazione HTTP di base, in questo modo:

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

... allora non solo l'URL è valido ma è registrato nel mio account tr.im. (Anche se response.code è ancora 200.)

Dev'esserci qualcosa di sbagliato nel mio codice (e non nell'API di tr.im), perché

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

...ritorna:

{"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"}

... e l'URL appare nel mio elenco di URL su http://tr.im/?page=1 .

E se corro:

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

... ancora una volta, ottengo:

{"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"}

Nota il codice è 201 e il messaggio è "URL tr.im già creato [yacitus]".

Non devo eseguire correttamente l'autenticazione HTTP di base (in entrambi i tentativi). Riesci a individuare il mio problema? Forse dovrei guardare e vedere cosa viene inviato tramite il filo? Non l'ho mai fatto prima. Ci sono API Python che posso usare (forse in pdb)? O c'è un altro strumento (preferibilmente per Mac OS X) che posso usare?


2
il sito deve restituire "WWW-Authenticate"e codice 401 prima che urllib2 (o httplib2) invii le tue credenziali. Vedi la mia risposta di seguito .
Mark Mikofski

Nota: questo servizio sembra essere inattivo.
Laurel

Risposte:


247

Sembra funzionare davvero bene (tratto da un altro thread)

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)

7
Invece di base64.encodestring e sostituisci, usa base64.standard_b64encode
Paweł Polewicz

5
request.add_header('Authorization', b'Basic ' + base64.b64encode(username + b':' + password))
jfs

1
Sulla base di questa risposta ho creato un pacchetto urllib2_prior_auth che non ha dipendenze al di fuori di stdlib e provo a inviare la modifica pertinente a stdlib .
mcepl

5
O anche più breve / evitando un'importazione: request.add_header ('Authorization', b'Basic '+ (username + b': '+ password) .encode (' base64 '))
makapuf

20

Soluzione davvero economica:

urllib.urlopen('http://user:xxxx@api.tr.im/api')

(che potresti decidere non è adatto per una serie di motivi, come la sicurezza dell'URL)

Esempio di API Github :

>>> import urllib, json
>>> result = urllib.urlopen('https://personal-access-token:x-oauth-basic@api.github.com/repos/:owner/:repo')
>>> r = json.load(result.fp)
>>> result.close()

Ci sono dei vantaggi rispetto all'utilizzo di parametri della stringa di query?
Daryl Spitzer

1
Daryl: se funziona, direi che è un vantaggio sì, e probabilmente più sicuro degli argomenti della stringa di query poiché la maggior parte dei client http sono un po 'più attenti a come li gestiscono.
Ali Afshar

Probabilmente lo seguirò (quindi ottieni il mio voto positivo), ma mi piacerebbe comunque capire cosa c'è che non va nel mio codice (quindi questa non sarà la mia risposta accettata).
Daryl Spitzer

36
Questo restituisce un errore ... InvalidURL: nonnumeric port: 'xxxx@api.tr.im/api'
Nick Bolton

5
@nbolton assicurati di non utilizzare urllib2.urlopen (url)
CantGetANick

13

Dai un'occhiata a questa risposta al post SO e guarda anche questo tutorial di autenticazione di base dal manuale mancante di urllib2 .

Affinché l'autenticazione di base di urllib2 funzioni, la risposta http deve contenere il codice HTTP 401 Non autorizzato e una chiave "WWW-Authenticate"con il valore "Basic"altrimenti, Python non invierà le tue informazioni di accesso e dovrai usare Richieste o urllib.urlopen(url)con il tuo accesso nel url o aggiungi un'intestazione come nella risposta di @ Flowpoke .

Puoi visualizzare il tuo errore inserendo il tuo urlopenin un blocco try:

try:
    urllib2.urlopen(urllib2.Request(url))
except urllib2.HTTPError, e:
    print e.headers
    print e.headers.has_key('WWW-Authenticate')

Questo mi ha aiutato perché la stampa delle intestazioni mi ha portato a rendermi conto di aver digitato il regno di autenticazione. +1
spazio libero

7

Il modo consigliato è usare il requestsmodulo :

#!/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

Ecco una urllib2variante basata su Python 2/3 compatibile con un'unica fonte :

#!/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+ introduceHTTPPasswordMgrWithPriorAuth() che consente:

..per eliminare la gestione della risposta 401 non necessaria o per inviare incondizionatamente le credenziali alla prima richiesta per comunicare con i server che restituiscono una risposta 404 invece di una 401 se l'intestazione di autorizzazione non viene inviata ..

#!/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()

In questo caso è facile da sostituire HTTPBasicAuthHandler()con ProxyBasicAuthHandler()se necessario.


4

Suggerirei che la soluzione corrente sia quella di utilizzare il mio pacchetto urllib2_prior_auth che risolve questo problema abbastanza bene (lavoro sull'inclusione nella lib standard.


1
È stato incluso in Python 3.5 comeurrlib.request.HTTPBasicPriorAuthHandler
mcepl

3

Si applicano le stesse soluzioni del problema di autenticazione di base urllib2 di Python .

vedere https://stackoverflow.com/a/24048852/1733117 ; puoi sottoclasse urllib2.HTTPBasicAuthHandlerper aggiungere l' Authorizationintestazione a ciascuna richiesta che corrisponde all'URL noto.

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

La chiamata a stripridondante non è dopo b64encode?
Mihai Todor

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.