Разбор заголовка авторизации HTTP-запроса с Python - PullRequest
12 голосов
/ 29 августа 2009

Мне нужно взять такой заголовок:

 Authorization: Digest qop="chap",
     realm="testrealm@host.com",
     username="Foobear",
     response="6629fae49393a05397450978507c4ef1",
     cnonce="5ccc069c403ebaf9f0171e9517f40e41"

И проанализируйте это с помощью Python:

{'protocol':'Digest',
  'qop':'chap',
  'realm':'testrealm@host.com',
  'username':'Foobear',
  'response':'6629fae49393a05397450978507c4ef1',
  'cnonce':'5ccc069c403ebaf9f0171e9517f40e41'}

Есть ли библиотека для этого или что-то, на что я мог бы взглянуть для вдохновения?

Я делаю это на Google App Engine, и я не уверен, доступна ли библиотека Pyparsing, но, возможно, я мог бы включить ее в свое приложение, если это лучшее решение.

В настоящее время я создаю свой собственный объект MyHeaderParser и использую его с методом Redu () в строке заголовка. Это работает, но очень хрупко.

Блестящее решение от Нади ниже:

import re

reg = re.compile('(\w+)[=] ?"?(\w+)"?')

s = """Digest
realm="stackoverflow.com", username="kixx"
"""

print str(dict(reg.findall(s)))

Ответы [ 10 ]

12 голосов
/ 29 августа 2009

Небольшое регулярное выражение:

import re
reg=re.compile('(\w+)[:=] ?"?(\w+)"?')

>>>dict(reg.findall(headers))

{'username': 'Foobear', 'realm': 'testrealm', 'qop': 'chap', 'cnonce': '5ccc069c403ebaf9f0171e9517f40e41', 'response': '6629fae49393a05397450978507c4ef1', 'Authorization': 'Digest'}
9 голосов
/ 29 августа 2009

Вы также можете использовать urllib2, как CheryPy .

вот фрагмент:

input= """
 Authorization: Digest qop="chap",
     realm="testrealm@host.com",
     username="Foobear",
     response="6629fae49393a05397450978507c4ef1",
     cnonce="5ccc069c403ebaf9f0171e9517f40e41"
"""
import urllib2
field, sep, value = input.partition("Authorization: Digest ")
if value:
    items = urllib2.parse_http_list(value)
    opts = urllib2.parse_keqv_list(items)
    opts['protocol'] = 'Digest'
    print opts

выводит:

{'username': 'Foobear', 'protocol': 'Digest', 'qop': 'chap', 'cnonce': '5ccc069c403ebaf9f0171e9517f40e41', 'realm': 'testrealm@host.com', 'response': '6629fae49393a05397450978507c4ef1'}
3 голосов
/ 04 сентября 2009

Вот моя попытка разбора:

text = """Authorization: Digest qop="chap",
    realm="testrealm@host.com",     
    username="Foobear",     
    response="6629fae49393a05397450978507c4ef1",     
    cnonce="5ccc069c403ebaf9f0171e9517f40e41" """

from pyparsing import *

AUTH = Keyword("Authorization")
ident = Word(alphas,alphanums)
EQ = Suppress("=")
quotedString.setParseAction(removeQuotes)

valueDict = Dict(delimitedList(Group(ident + EQ + quotedString)))
authentry = AUTH + ":" + ident("protocol") + valueDict

print authentry.parseString(text).dump()

который печатает:

['Authorization', ':', 'Digest', ['qop', 'chap'], ['realm', 'testrealm@host.com'],
 ['username', 'Foobear'], ['response', '6629fae49393a05397450978507c4ef1'], 
 ['cnonce', '5ccc069c403ebaf9f0171e9517f40e41']]
- cnonce: 5ccc069c403ebaf9f0171e9517f40e41
- protocol: Digest
- qop: chap
- realm: testrealm@host.com
- response: 6629fae49393a05397450978507c4ef1
- username: Foobear

Я не знаком с RFC, но я надеюсь, что это поможет вам.

1 голос
/ 23 сентября 2018

Более старый вопрос, но я нашел его очень полезным.

Мне понадобился парсер для обработки любого правильно сформированного заголовка авторизации, как определено в RFC7235 (поднимите руку, если вам нравится читать ABNF).

Authorization = credentials

BWS = <BWS, see [RFC7230], Section 3.2.3>

OWS = <OWS, see [RFC7230], Section 3.2.3>

Proxy-Authenticate = *( "," OWS ) challenge *( OWS "," [ OWS
 challenge ] )
Proxy-Authorization = credentials

WWW-Authenticate = *( "," OWS ) challenge *( OWS "," [ OWS challenge
 ] )

auth-param = token BWS "=" BWS ( token / quoted-string )
auth-scheme = token

challenge = auth-scheme [ 1*SP ( token68 / [ ( "," / auth-param ) *(
 OWS "," [ OWS auth-param ] ) ] ) ]
credentials = auth-scheme [ 1*SP ( token68 / [ ( "," / auth-param )
 *( OWS "," [ OWS auth-param ] ) ] ) ]

quoted-string = <quoted-string, see [RFC7230], Section 3.2.6>

token = <token, see [RFC7230], Section 3.2.6>
token68 = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" )
 *"="

Начиная с ответа PaulMcG , я придумал следующее:

import pyparsing as pp

tchar = '!#$%&\'*+-.^_`|~' + pp.nums + pp.alphas
t68char = '-._~+/' + pp.nums + pp.alphas

token = pp.Word(tchar)
token68 = pp.Combine(pp.Word(t68char) + pp.ZeroOrMore('='))

scheme = token('scheme')

header = pp.Keyword('Authorization')
name = pp.Word(pp.alphas, pp.alphanums)
value = pp.quotedString.setParseAction(pp.removeQuotes)
name_value_pair = name + pp.Suppress('=') + value
params = pp.Dict(pp.delimitedList(pp.Group(name_value_pair)))

credentials = scheme + (token68('token') ^ params('params'))

auth_parser = header + pp.Suppress(':') + credentials

Это позволяет анализировать любой заголовок авторизации:

parsed = auth_parser.parseString('Authorization: Basic Zm9vOmJhcg==')
print('Authenticating with {0} scheme, token: {1}'.format(parsed['scheme'], parsed['token']))

который выводит:

Authenticating with Basic scheme, token: Zm9vOmJhcg==

Объединение всего этого в класс Authenticator:

import pyparsing as pp
from base64 import b64decode
import re

class Authenticator:
    def __init__(self):
        """
        Use pyparsing to create a parser for Authentication headers
        """
        tchar = "!#$%&'*+-.^_`|~" + pp.nums + pp.alphas
        t68char = '-._~+/' + pp.nums + pp.alphas

        token = pp.Word(tchar)
        token68 = pp.Combine(pp.Word(t68char) + pp.ZeroOrMore('='))

        scheme = token('scheme')

        auth_header = pp.Keyword('Authorization')
        name = pp.Word(pp.alphas, pp.alphanums)
        value = pp.quotedString.setParseAction(pp.removeQuotes)
        name_value_pair = name + pp.Suppress('=') + value
        params = pp.Dict(pp.delimitedList(pp.Group(name_value_pair)))

        credentials = scheme + (token68('token') ^ params('params'))

        # the moment of truth...
        self.auth_parser = auth_header + pp.Suppress(':') + credentials


    def authenticate(self, auth_header):
        """
        Parse auth_header and call the correct authentication handler
        """
        authenticated = False
        try:
            parsed = self.auth_parser.parseString(auth_header)
            scheme = parsed['scheme']
            details = parsed['token'] if 'token' in parsed.keys() else parsed['params']

            print('Authenticating using {0} scheme'.format(scheme))
            try:
                safe_scheme = re.sub("[!#$%&'*+-.^_`|~]", '_', scheme.lower())
                handler = getattr(self, 'auth_handle_' + safe_scheme)
                authenticated = handler(details)
            except AttributeError:
                print('This is a valid Authorization header, but we do not handle this scheme yet.')

        except pp.ParseException as ex:
            print('Not a valid Authorization header')
            print(ex)

        return authenticated


    # The following methods are fake, of course.  They should use what's passed
    # to them to actually authenticate, and return True/False if successful.
    # For this demo I'll just print some of the values used to authenticate.
    @staticmethod
    def auth_handle_basic(token):
        print('- token is {0}'.format(token))
        try:
            username, password = b64decode(token).decode().split(':', 1)
        except Exception:
            raise DecodeError
        print('- username is {0}'.format(username))
        print('- password is {0}'.format(password))
        return True

    @staticmethod
    def auth_handle_bearer(token):
        print('- token is {0}'.format(token))
        return True

    @staticmethod
    def auth_handle_digest(params):
        print('- username is {0}'.format(params['username']))
        print('- cnonce is {0}'.format(params['cnonce']))
        return True

    @staticmethod
    def auth_handle_aws4_hmac_sha256(params):
        print('- Signature is {0}'.format(params['Signature']))
        return True

Для проверки этого класса:

tests = [
    'Authorization: Digest qop="chap", realm="testrealm@example.com", username="Foobar", response="6629fae49393a05397450978507c4ef1", cnonce="5ccc069c403ebaf9f0171e9517f40e41"',
    'Authorization: Bearer cn389ncoiwuencr',
    'Authorization: Basic Zm9vOmJhcg==',
    'Authorization: AWS4-HMAC-SHA256 Credential="AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request", SignedHeaders="host;range;x-amz-date", Signature="fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024"',
    'Authorization: CrazyCustom foo="bar", fizz="buzz"',
]

authenticator = Authenticator()

for test in tests:
    authenticator.authenticate(test)
    print()

Какие выходы:

Authenticating using Digest scheme
- username is Foobar
- cnonce is 5ccc069c403ebaf9f0171e9517f40e41

Authenticating using Bearer scheme
- token is cn389ncoiwuencr

Authenticating using Basic scheme
- token is Zm9vOmJhcg==
- username is foo
- password is bar

Authenticating using AWS4-HMAC-SHA256 scheme
- signature is fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024

Authenticating using CrazyCustom scheme 
This is a valid Authorization header, but we do not handle this scheme yet.

В будущем, если мы захотим работать с CrazyCustom, мы просто добавим

def auth_handle_crazycustom(params):
1 голос
/ 13 сентября 2011

Регулярное выражение Нади соответствует буквенно-цифровым символам для значения параметра. Это означает, что он не может проанализировать как минимум два поля. А именно, URI и QOP. Согласно RFC 2617 поле uri является дубликатом строки в строке запроса (то есть первой строки HTTP-запроса). И qop не сможет правильно проанализировать, если значение «auth-int» из-за не алфавитно-цифровых символов «-».

Это модифицированное регулярное выражение позволяет URI (или любому другому значению) содержать что угодно, кроме '' (пробел), '"' (qoute) или ',' (запятая). Это, вероятно, более допустимо, чем должно но не должно вызывать проблем с правильно сформированными HTTP-запросами.

reg re.compile('(\w+)[:=] ?"?([^" ,]+)"?')

Бонусный совет: оттуда довольно просто преобразовать пример кода в RFC-2617 в python. Используя API md5 Python, MD5Init () становится m = md5.new (), MD5Update () становится m.update (), а MD5Final () становится m.digest () ".

1 голос
/ 14 мая 2010

Поле заголовка авторизации http-дайджеста немного странно. Его формат аналогичен формату полей заголовка Cache-Control и Content-Type rfc 2616 , но достаточно отличается, чтобы быть несовместимым. Если вы все еще ищете библиотеку, которая немного умнее и удобнее для чтения, чем регулярное выражение, вы можете попробовать удалить часть Authorization: Digest с помощью str.split () и проанализировать остальные с помощью parse_dict_header () из модуля Werkzeug http. (Werkzeug может быть установлен на App Engine.)

1 голос
/ 29 августа 2009

Ваша оригинальная концепция использования PyParsing будет лучшим подходом. То, о чем вы явно просили, - это то, что требует грамматики ... то есть регулярное выражение или простая процедура синтаксического анализа всегда будут хрупкими, и это звучит так, как будто вы пытаетесь избежать этого.

Похоже, что получить pyparsing на Google App Engine легко: Как настроить PyParsing на Google App Engine?

Итак, я бы пошел с этим, а затем реализовал полную поддержку заголовка HTTP аутентификации / авторизации от rfc2617.

1 голос
/ 29 августа 2009

Я бы порекомендовал найти правильную библиотеку для разбора http-заголовков, к сожалению, я не могу перезапустить ни одну. (

Какое-то время проверяйте фрагмент кода ниже (в основном он должен работать):

input= """
 Authorization: Digest qop="chap",
     realm="testrealm@host.com",
     username="Foob,ear",
     response="6629fae49393a05397450978507c4ef1",
     cnonce="5ccc069c403ebaf9f0171e9517f40e41"
"""

field, sep, value = input.partition(":")
if field.endswith('Authorization'):
   protocol, sep, opts_str = value.strip().partition(" ")

   opts = {}
   for opt in opts_str.split(",\n"):
        key, value = opt.strip().split('=')
        key = key.strip(" ")
        value = value.strip(' "')
        opts[key] = value

   opts['protocol'] = protocol

   print opts
1 голос
/ 29 августа 2009

Если эти компоненты всегда будут присутствовать, то регулярное выражение сделает свое дело:

test = '''Authorization: Digest qop="chap", realm="testrealm@host.com", username="Foobear", response="6629fae49393a05397450978507c4ef1", cnonce="5ccc069c403ebaf9f0171e9517f40e41"'''

import re

re_auth = re.compile(r"""
    Authorization:\s*(?P<protocol>[^ ]+)\s+
    qop="(?P<qop>[^"]+)",\s+
    realm="(?P<realm>[^"]+)",\s+
    username="(?P<username>[^"]+)",\s+
    response="(?P<response>[^"]+)",\s+
    cnonce="(?P<cnonce>[^"]+)"
    """, re.VERBOSE)

m = re_auth.match(test)
print m.groupdict()

производит:

{ 'username': 'Foobear', 
  'protocol': 'Digest', 
  'qop': 'chap', 
  'cnonce': '5ccc069c403ebaf9f0171e9517f40e41', 
  'realm': 'testrealm@host.com', 
  'response': '6629fae49393a05397450978507c4ef1'
}
0 голосов
/ 29 августа 2009

Если ваш ответ состоит из одной строки, которая никогда не меняется и содержит столько строк, сколько выражений для совпадения , вы можете разбить его на массив в новых строках с именем authentication_array и использовать регулярные выражения :

pattern_array = ['qop', 'realm', 'username', 'response', 'cnonce']
i = 0
parsed_dict = {}

for line in authentication_array:
    pattern = "(" + pattern_array[i] + ")" + "=(\".*\")" # build a matching pattern
    match = re.search(re.compile(pattern), line)         # make the match
    if match:
        parsed_dict[match.group(1)] = match.group(2)
    i += 1
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...