Python urllib2, authentification HTTP de base et tr.im

84

Je joue en essayant d'écrire du code pour utiliser les API tr.im pour raccourcir une URL.

Après avoir lu http://docs.python.org/library/urllib2.html , j'ai essayé:

   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 est 200 (je pense qu'il devrait être 202). url est valide, mais l'authentification HTTP de base ne semble pas avoir fonctionné, car l'URL raccourcie ne figure pas dans ma liste d'URL (sur http://tr.im/?page=1 ).

Après avoir lu http://www.voidspace.org.uk/python/articles/authentication.shtml#doing-it-properly, j'ai également essayé:

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

Mais j'obtiens les mêmes résultats. (response.code est 200 et l'URL est valide, mais pas enregistrée dans mon compte à http://tr.im/ .)

Si j'utilise des paramètres de chaîne de requête au lieu de l'authentification HTTP de base, comme ceci:

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

... alors non seulement l'URL est valide mais elle est enregistrée dans mon compte tr.im. (Bien que response.code soit toujours 200.)

Il doit y avoir quelque chose qui ne va pas avec mon code (et pas l'API de tr.im), car

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

...Retour:

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

... et l'URL apparaît dans ma liste d'URL sur http://tr.im/?page=1 .

Et si je cours:

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

... encore une fois, je reçois:

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

Le code de note est 201 et le message est "URL tr.im déjà créée [yacitus]".

Je ne dois pas faire correctement l'authentification HTTP de base (dans les deux tentatives). Pouvez-vous repérer mon problème? Peut-être que je devrais regarder et voir ce qui est envoyé sur le fil? Je n'ai jamais fait ça avant. Y a-t-il des API Python que je peux utiliser (peut-être dans pdb)? Ou y a-t-il un autre outil (de préférence pour Mac OS X) que je peux utiliser?

Daryl Spitzer
la source
2
le site doit renvoyer "WWW-Authenticate"et le code 401 avant que urllib2 (ou httplib2) envoie vos identifiants. Voir ma réponse ci-dessous .
Mark Mikofski
Remarque: ce service semble être obsolète.
Laurel

Réponses:

246

Cela semble très bien fonctionner (tiré d'un autre fil)

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
la source
7
Au lieu de base64.encodestring et replace, utilisez base64.standard_b64encode
Paweł Polewicz
5
request.add_header('Authorization', b'Basic ' + base64.b64encode(username + b':' + password))
jfs
1
Sur la base de cette réponse, j'ai créé un package urllib2_prior_auth qui n'a pas de dépendances en dehors de stdlib, et j'essaie de pousser le changement pertinent vers stdlib .
mcepl
5
Ou encore plus court / éviter une importation: request.add_header ('Authorization', b'Basic '+ (username + b': '+ password) .encode (' base64 '))
makapuf
20

Solution vraiment bon marché:

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

(ce que vous pouvez décider ne convient pas pour un certain nombre de raisons, comme la sécurité de l'url)

Exemple d'API 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
la source
Y a-t-il des avantages à cela par rapport à l'utilisation de paramètres de chaîne de requête?
Daryl Spitzer
1
Daryl: si cela fonctionne, je dirais que c'est un avantage oui, et probablement plus sûr que les arguments de chaîne de requête car la plupart des clients http sont un peu plus prudents sur la façon dont ils les gèrent.
Ali Afshar
J'irai probablement avec ceci (donc vous obtenez mon vote favorable), mais j'aimerais toujours comprendre ce qui ne va pas avec mon code (donc ce ne sera pas ma réponse acceptée).
Daryl Spitzer
36
Cela renvoie une erreur ... InvalidURL: port non numérique: '[email protected]/api'
Nick Bolton
5
@nbolton assurez-vous que vous n'utilisez pas urllib2.urlopen (url)
CantGetANick
13

Jetez un œil à cette réponse post SO et regardez également ce didacticiel d'authentification de base du manuel manquant urllib2 .

Pour que l'authentification de base urllib2 fonctionne, la réponse http doit contenir le code HTTP 401 Unauthorized et une clé "WWW-Authenticate"avec la valeur "Basic"sinon, Python n'enverra pas vos informations de connexion, et vous devrez soit utiliser Requests , soit urllib.urlopen(url)avec votre login dans le url, ou ajoutez un en-tête comme dans la réponse de @ Flowpoke .

Vous pouvez afficher votre erreur en mettant votre urlopen dans un bloc try:

try:
    urllib2.urlopen(urllib2.Request(url))
except urllib2.HTTPError, e:
    print e.headers
    print e.headers.has_key('WWW-Authenticate')
Mark Mikofski
la source
Cela m'a aidé car l'impression des en-têtes m'a amené à réaliser que j'avais tapé dans le domaine d'authentification. +1
freespace
7

La méthode recommandée est d'utiliser le requestsmodule :

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

Voici une urllib2variante basée sur la compatibilité Python 2/3 à source unique :

#!/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+ introduitHTTPPasswordMgrWithPriorAuth() qui permet:

..pour éliminer le traitement inutile des réponses 401, ou pour envoyer sans condition des informations d'identification sur la première requête afin de communiquer avec les serveurs qui renvoient une réponse 404 au lieu d'une 401 si l'en-tête d'autorisation n'est pas envoyé.

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

Il est facile de remplacer HTTPBasicAuthHandler() avec ProxyBasicAuthHandler()si nécessaire dans ce cas.

jfs
la source
4

Je suggérerais que la solution actuelle soit d'utiliser mon package urllib2_prior_auth qui résout ce problème assez bien (je travaille sur l' inclusion dans la bibliothèque standard.

mcepl
la source
1
Il a été inclus dans Python 3.5 en tant queurrlib.request.HTTPBasicPriorAuthHandler
mcepl
3

Les mêmes solutions que Python urllib2 Basic Auth Problem s'appliquent.

voir https://stackoverflow.com/a/24048852/1733117 ; vous pouvez sous-classerurllib2.HTTPBasicAuthHandler pour ajouter l'en- Authorizationtête à chaque demande qui correspond à l'url connue.

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
la source
L'appel n'est-il pas stripredondant après b64encode?
Mihai Todor