Python urllib2, autenticación HTTP básica y tr.im

84

Estoy jugando, tratando de escribir código para usar las API de tr.im para acortar una URL.

Después de leer http://docs.python.org/library/urllib2.html , probé:

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

El código de respuesta es 200 (creo que debería ser 202). url es válida, pero la autenticación HTTP básica no parece haber funcionado, porque la URL abreviada no está en mi lista de URL (en http://tr.im/?page=1 ).

Después de leer http://www.voidspace.org.uk/python/articles/authentication.shtml#doing-it-properly también probé:

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

Pero obtengo los mismos resultados. (El código de respuesta es 200 y la URL es válida, pero no está registrada en mi cuenta en http://tr.im/ ).

Si uso parámetros de cadena de consulta en lugar de la autenticación HTTP básica, así:

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

... entonces no solo la URL es válida, sino que está registrada en mi cuenta tr.im. (Aunque el código de respuesta sigue siendo 200).

Sin embargo, debe haber algo mal con mi código (y no con la API de tr.im), porque

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

...devoluciones:

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

... y la URL aparece en mi lista de URL en http://tr.im/?page=1 .

Y si corro:

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

... de nuevo, obtengo:

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

Tenga en cuenta que el código es 201 y el mensaje es "URL de tr.im ya creada [yacitus]".

No debo realizar correctamente la autenticación HTTP básica (en ninguno de los intentos). ¿Puedes detectar mi problema? ¿Quizás debería mirar y ver qué se envía por cable? Nunca había hecho eso antes. ¿Hay API de Python que pueda usar (quizás en pdb)? ¿O hay otra herramienta (preferiblemente para Mac OS X) que pueda usar?

Daryl Spitzer
fuente
2
el sitio debe regresar "WWW-Authenticate"y el código 401 antes de que urllib2 (o httplib2) envíe sus credenciales. Vea mi respuesta a continuación .
Mark Mikofski
Nota: este servicio parece haber desaparecido.
Laurel

Respuestas:

246

Esto parece funcionar muy bien (tomado de otro hilo)

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
fuente
7
En lugar de base64.encodestring y reemplazar, use base64.standard_b64encode
Paweł Polewicz
5
request.add_header('Authorization', b'Basic ' + base64.b64encode(username + b':' + password))
jfs
1
En base a esta respuesta, he creado un paquete urllib2_prior_auth que no tiene dependencias fuera de stdlib, e intento enviar el cambio relevante a stdlib .
mcepl
5
O incluso más corto / evitando una importación: request.add_header ('Authorization', b'Basic '+ (username + b': '+ password) .encode (' base64 '))
makapuf
20

Solución realmente barata:

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

(que puede decidir que no es adecuado por varias razones, como la seguridad de la URL)

Ejemplo de API de Github :

>>> import urllib, json
>>> result = urllib.urlopen('https://personal-access-token:[email protected]/repos/:owner/:repo')
>>> r = json.load(result.fp)
>>> result.close()
Ali Afshar
fuente
¿Hay alguna ventaja en esto sobre el uso de parámetros de cadena de consulta?
Daryl Spitzer
1
Daryl: si funciona, diría que es una ventaja, sí, y probablemente más seguro que los argumentos de cadena de consulta, ya que la mayoría de los clientes http son un poco más cuidadosos sobre cómo los manejan.
Ali Afshar
Probablemente iré con esto (para que obtengas mi voto a favor), pero aún me gustaría averiguar qué está mal con mi código (para que esta no sea mi respuesta aceptada).
Daryl Spitzer
36
Esto devuelve un error ... URL no válida: puerto no numérico: '[email protected]/api'
Nick Bolton
5
@nbolton asegúrese de no estar usando urllib2.urlopen (url)
CantGetANick
13

Eche un vistazo a esta respuesta de la publicación SO y también a este tutorial de autenticación básico del manual faltante de urllib2 .

Para que funcione la autenticación básica de urllib2, la respuesta http debe contener el código HTTP 401 No autorizado y una clave "WWW-Authenticate"con el valor; de lo "Basic"contrario, Python no enviará su información de inicio de sesión y deberá utilizar Solicitudes o urllib.urlopen(url)con su inicio de sesión en el url, o agregue un encabezado como en la respuesta de @ Flowpoke .

Puede ver su error colocando su urlopenen un bloque de prueba:

try:
    urllib2.urlopen(urllib2.Request(url))
except urllib2.HTTPError, e:
    print e.headers
    print e.headers.has_key('WWW-Authenticate')
Mark Mikofski
fuente
Esto me ayudó porque imprimir los encabezados me llevó a darme cuenta de que había escrito el reino de autenticación. +1
espacio libre
7

La forma recomendada es utilizar el requestsmódulo :

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

Aquí hay una urllib2variante basada en una sola fuente compatible con Python 2/3 :

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

... para eliminar el manejo innecesario de respuestas 401, o para enviar incondicionalmente credenciales en la primera solicitud para comunicarse con servidores que devuelven una respuesta 404 en lugar de una 401 si no se envía el encabezado de autorización.

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

Es fácil de reemplazar HTTPBasicAuthHandler()con ProxyBasicAuthHandler()si es necesario en este caso.

jfs
fuente
4

Sugeriría que la solución actual es usar mi paquete urllib2_prior_auth que resuelve esto bastante bien (trabajo en la inclusión en el estándar lib.

mcepl
fuente
1
Se ha incluido en Python 3.5 comourrlib.request.HTTPBasicPriorAuthHandler
mcepl
3

Se aplican las mismas soluciones que el problema de autenticación básica urllib2 de Python .

ver https://stackoverflow.com/a/24048852/1733117 ; puede crear urllib2.HTTPBasicAuthHandleruna subclase para agregar el Authorizationencabezado a cada solicitud que coincida con la URL conocida.

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
fuente
¿No es stripredundante la llamada después b64encode?
Mihai Todor