Python urllib2, базовая HTTP-аутентификация и tr.im

85

Я играю, пытаясь написать код для использования API tr.im для сокращения URL-адреса.

Прочитав http://docs.python.org/library/urllib2.html , я попробовал:

   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 (думаю, должно быть 202). URL-адрес действителен, но базовая HTTP-аутентификация, похоже, не сработала, потому что сокращенного URL-адреса нет в моем списке URL-адресов ( http://tr.im/?page=1 ).

Прочитав http://www.voidspace.org.uk/python/articles/authentication.shtml#doing-it-properly, я также попробовал:

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

Но получаю те же результаты. (response.code - 200, а url действителен, но не записан в моей учетной записи на http://tr.im/ .)

Если я использую параметры строки запроса вместо базовой HTTP-аутентификации, например:

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

... тогда URL-адрес не только действителен, но и записан в моей учетной записи tr.im. (Хотя response.code по-прежнему 200.)

Что-то должно быть не так с моим кодом (а не с API tr.im), потому что

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

... возвращает:

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

... и URL-адрес действительно отображается в моем списке URL-адресов на http://tr.im/?page=1 .

А если я бегу:

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

... снова я получаю:

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

Код заметки - 201, а сообщение - «URL tr.im уже создан [yacitus]».

Я не должен правильно выполнять базовую аутентификацию HTTP (ни в одной из этих попыток). Вы можете определить мою проблему? Может, мне стоит посмотреть, что передается по сети? Я никогда раньше этого не делал. Могу ли я использовать Python API (возможно, в pdb)? Или есть другой инструмент (желательно для Mac OS X), который я могу использовать?

Дэрил Спитцер
источник
2
сайт должен вернуть "WWW-Authenticate"код 401, прежде чем urllib2 (или httplib2) отправит ваши учетные данные. Смотрите мой ответ ниже .
Марк Микофски
Примечание: эта служба кажется неработающей.
Лорел

Ответы:

247

Кажется, это действительно хорошо работает (взято из другого потока)

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
Вместо base64.encodestring и replace используйте base64.standard_b64encode
Paweł Polewicz
5
request.add_header('Authorization', b'Basic ' + base64.b64encode(username + b':' + password))
jfs
1
На основе этого ответа я создал пакет urllib2_prior_auth, у которого нет зависимостей вне stdlib, и я пытаюсь внести соответствующие изменения в stdlib .
mcepl
5
Или даже короче / избегая импорта: request.add_header ('Authorization', b'Basic '+ (username + b': '+ password) .encode (' base64 '))
makapuf
20

Действительно дешевое решение:

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

(который вы можете решить, не подходит по ряду причин, например, безопасность URL-адреса)

Пример Github API :

>>> 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()
Али Афшар
источник
Есть ли у этого преимущества перед использованием параметров строки запроса?
Дэрил Спитцер,
1
Дэрил: если это сработает, я бы сказал, что это преимущество, да и, вероятно, более безопасно, чем аргументы строки запроса, поскольку большинство http-клиентов немного более осторожны в том, как они их обрабатывают.
Али Афшар,
Я, вероятно, пойду с этим (так что вы получите мой голос), но я все равно хотел бы выяснить, что не так с моим кодом (так что это не будет моим принятым ответом).
Дэрил Спитцер,
36
Это возвращает ошибку ... InvalidURL: nonnumeric port: 'xxxx@api.tr.im/api'
Ник Болтон
5
@nbolton, убедитесь, что вы не используете urllib2.urlopen (url)
CantGetANick
13

Взгляните на этот ответ на сообщение SO, а также посмотрите этот учебник по базовой аутентификации из руководства по отсутствию urllib2 .

Чтобы базовая аутентификация urllib2 работала, HTTP-ответ должен содержать HTTP-код 401 Unauthorized и ключ "WWW-Authenticate"со значением, "Basic"иначе Python не отправит ваши данные для входа, и вам нужно будет либо использовать запросы , либо urllib.urlopen(url)указать свой логин в URL, или добавить заголовок , как в @ Flowpoke в ответ .

Вы можете просмотреть свою ошибку, поместив ее urlopenв блок попытки:

try:
    urllib2.urlopen(urllib2.Request(url))
except urllib2.HTTPError, e:
    print e.headers
    print e.headers.has_key('WWW-Authenticate')
Марк Микофски
источник
Это помогло мне, потому что печать заголовков заставила меня понять, что я опечатал область аутентификации. +1
freespace
7

Рекомендуемый способ - использовать requestsмодуль :

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

Вот urllib2вариант с одним исходным кодом, совместимый с 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+ представляет,HTTPPasswordMgrWithPriorAuth() что позволяет:

..чтобы исключить ненужную обработку ответа 401 или безоговорочно отправлять учетные данные при первом запросе, чтобы взаимодействовать с серверами, которые возвращают ответ 404 вместо 401, если заголовок авторизации не отправлен.

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

Это легко заменить HTTPBasicAuthHandler()с , ProxyBasicAuthHandler()если это необходимо в данном случае.

jfs
источник
4

Я бы предположил, что текущее решение - использовать мой пакет urllib2_prior_auth, который решает эту проблему довольно хорошо (я работаю над включением в стандартный lib.

mcepl
источник
1
Он был включен в Python 3.5 какurrlib.request.HTTPBasicPriorAuthHandler
mcepl
3

Применяются те же решения, что и у Python urllib2 Basic Auth .

см. https://stackoverflow.com/a/24048852/1733117 ; вы можете создать подкласс, urllib2.HTTPBasicAuthHandlerчтобы добавить Authorizationзаголовок к каждому запросу, который соответствует известному 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
днозай
источник
Разве после этого призыв к stripизбыточности не является b64encode?
Михай Тодор