Integrated my history into data
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
#-*- coding: utf-8 -*-
|
||||
|
||||
__all__ = [
|
||||
'constants',
|
||||
'discover',
|
||||
'etxrd',
|
||||
'filters',
|
||||
'manager',
|
||||
'parsehtml',
|
||||
'services',
|
||||
'xri',
|
||||
'xrires',
|
||||
]
|
||||
|
||||
version_info = (2, 0, 0)
|
||||
|
||||
__version__ = ".".join(str(x) for x in version_info)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,137 @@
|
||||
"""Functions for generating and parsing HTTP Accept: headers for
|
||||
supporting server-directed content negotiation.
|
||||
"""
|
||||
|
||||
|
||||
def generateAcceptHeader(*elements):
|
||||
"""Generate an accept header value
|
||||
|
||||
[str or (str, float)] -> str
|
||||
"""
|
||||
parts = []
|
||||
for element in elements:
|
||||
if type(element) is str:
|
||||
qs = "1.0"
|
||||
mtype = element
|
||||
else:
|
||||
mtype, q = element
|
||||
q = float(q)
|
||||
if q > 1 or q <= 0:
|
||||
raise ValueError('Invalid preference factor: %r' % q)
|
||||
|
||||
qs = '%0.1f' % (q, )
|
||||
|
||||
parts.append((qs, mtype))
|
||||
|
||||
parts.sort()
|
||||
chunks = []
|
||||
for q, mtype in parts:
|
||||
if q == '1.0':
|
||||
chunks.append(mtype)
|
||||
else:
|
||||
chunks.append('%s; q=%s' % (mtype, q))
|
||||
|
||||
return ', '.join(chunks)
|
||||
|
||||
|
||||
def parseAcceptHeader(value):
|
||||
"""Parse an accept header, ignoring any accept-extensions
|
||||
|
||||
returns a list of tuples containing main MIME type, MIME subtype,
|
||||
and quality markdown.
|
||||
|
||||
str -> [(str, str, float)]
|
||||
"""
|
||||
chunks = [chunk.strip() for chunk in value.split(',')]
|
||||
accept = []
|
||||
for chunk in chunks:
|
||||
parts = [s.strip() for s in chunk.split(';')]
|
||||
|
||||
mtype = parts.pop(0)
|
||||
if '/' not in mtype:
|
||||
# This is not a MIME type, so ignore the bad data
|
||||
continue
|
||||
|
||||
main, sub = mtype.split('/', 1)
|
||||
|
||||
for ext in parts:
|
||||
if '=' in ext:
|
||||
k, v = ext.split('=', 1)
|
||||
if k == 'q':
|
||||
try:
|
||||
q = float(v)
|
||||
break
|
||||
except ValueError:
|
||||
# Ignore poorly formed q-values
|
||||
pass
|
||||
else:
|
||||
q = 1.0
|
||||
|
||||
accept.append((q, main, sub))
|
||||
|
||||
accept.sort()
|
||||
accept.reverse()
|
||||
return [(main, sub, q) for (q, main, sub) in accept]
|
||||
|
||||
|
||||
def matchTypes(accept_types, have_types):
|
||||
"""Given the result of parsing an Accept: header, and the
|
||||
available MIME types, return the acceptable types with their
|
||||
quality markdowns.
|
||||
|
||||
For example:
|
||||
|
||||
>>> acceptable = parseAcceptHeader('text/html, text/plain; q=0.5')
|
||||
>>> matchTypes(acceptable, ['text/plain', 'text/html', 'image/jpeg'])
|
||||
[('text/html', 1.0), ('text/plain', 0.5)]
|
||||
|
||||
|
||||
Type signature: ([(str, str, float)], [str]) -> [(str, float)]
|
||||
"""
|
||||
if not accept_types:
|
||||
# Accept all of them
|
||||
default = 1
|
||||
else:
|
||||
default = 0
|
||||
|
||||
match_main = {}
|
||||
match_sub = {}
|
||||
for (main, sub, q) in accept_types:
|
||||
if main == '*':
|
||||
default = max(default, q)
|
||||
continue
|
||||
elif sub == '*':
|
||||
match_main[main] = max(match_main.get(main, 0), q)
|
||||
else:
|
||||
match_sub[(main, sub)] = max(match_sub.get((main, sub), 0), q)
|
||||
|
||||
accepted_list = []
|
||||
order_maintainer = 0
|
||||
for mtype in have_types:
|
||||
main, sub = mtype.split('/')
|
||||
if (main, sub) in match_sub:
|
||||
q = match_sub[(main, sub)]
|
||||
else:
|
||||
q = match_main.get(main, default)
|
||||
|
||||
if q:
|
||||
accepted_list.append((1 - q, order_maintainer, q, mtype))
|
||||
order_maintainer += 1
|
||||
|
||||
accepted_list.sort()
|
||||
return [(mtype, q) for (_, _, q, mtype) in accepted_list]
|
||||
|
||||
|
||||
def getAcceptable(accept_header, have_types):
|
||||
"""Parse the accept header and return a list of available types in
|
||||
preferred order. If a type is unacceptable, it will not be in the
|
||||
resulting list.
|
||||
|
||||
This is a convenience wrapper around matchTypes and
|
||||
parseAcceptHeader.
|
||||
|
||||
(str, [str]) -> [str]
|
||||
"""
|
||||
accepted = parseAcceptHeader(accept_header)
|
||||
preferred = matchTypes(accepted, have_types)
|
||||
return [mtype for (mtype, _) in preferred]
|
||||
@@ -0,0 +1,12 @@
|
||||
__all__ = ['YADIS_HEADER_NAME', 'YADIS_CONTENT_TYPE', 'YADIS_ACCEPT_HEADER']
|
||||
from openid.yadis.accept import generateAcceptHeader
|
||||
|
||||
YADIS_HEADER_NAME = 'X-XRDS-Location'
|
||||
YADIS_CONTENT_TYPE = 'application/xrds+xml'
|
||||
|
||||
# A value suitable for using as an accept header when performing YADIS
|
||||
# discovery, unless the application has special requirements
|
||||
YADIS_ACCEPT_HEADER = generateAcceptHeader(
|
||||
('text/html', 0.3),
|
||||
('application/xhtml+xml', 0.5),
|
||||
(YADIS_CONTENT_TYPE, 1.0), )
|
||||
@@ -0,0 +1,169 @@
|
||||
# -*- test-case-name: openid.test.test_yadis_discover -*-
|
||||
__all__ = ['discover', 'DiscoveryResult', 'DiscoveryFailure']
|
||||
|
||||
from io import StringIO
|
||||
|
||||
from openid import fetchers
|
||||
|
||||
from openid.yadis.constants import \
|
||||
YADIS_HEADER_NAME, YADIS_CONTENT_TYPE, YADIS_ACCEPT_HEADER
|
||||
from openid.yadis.parsehtml import MetaNotFound, findHTMLMeta
|
||||
|
||||
|
||||
class DiscoveryFailure(Exception):
|
||||
"""Raised when a YADIS protocol error occurs in the discovery process"""
|
||||
identity_url = None
|
||||
|
||||
def __init__(self, message, http_response):
|
||||
Exception.__init__(self, message)
|
||||
self.http_response = http_response
|
||||
|
||||
|
||||
class DiscoveryResult(object):
|
||||
"""Contains the result of performing Yadis discovery on a URI"""
|
||||
|
||||
# The URI that was passed to the fetcher
|
||||
request_uri = None
|
||||
|
||||
# The result of following redirects from the request_uri
|
||||
normalized_uri = None
|
||||
|
||||
# The URI from which the response text was returned (set to
|
||||
# None if there was no XRDS document found)
|
||||
xrds_uri = None
|
||||
|
||||
# The content-type returned with the response_text
|
||||
content_type = None
|
||||
|
||||
# The document returned from the xrds_uri
|
||||
response_text = None
|
||||
|
||||
def __init__(self, request_uri):
|
||||
"""Initialize the state of the object
|
||||
|
||||
sets all attributes to None except the request_uri
|
||||
"""
|
||||
self.request_uri = request_uri
|
||||
|
||||
def usedYadisLocation(self):
|
||||
"""Was the Yadis protocol's indirection used?"""
|
||||
if self.xrds_uri is None:
|
||||
return False
|
||||
return self.normalized_uri != self.xrds_uri
|
||||
|
||||
def isXRDS(self):
|
||||
"""Is the response text supposed to be an XRDS document?"""
|
||||
return (self.usedYadisLocation() or
|
||||
self.content_type == YADIS_CONTENT_TYPE)
|
||||
|
||||
|
||||
def discover(uri):
|
||||
"""Discover services for a given URI.
|
||||
|
||||
@param uri: The identity URI as a well-formed http or https
|
||||
URI. The well-formedness and the protocol are not checked, but
|
||||
the results of this function are undefined if those properties
|
||||
do not hold.
|
||||
|
||||
@return: DiscoveryResult object
|
||||
|
||||
@raises Exception: Any exception that can be raised by fetching a URL with
|
||||
the given fetcher.
|
||||
@raises DiscoveryFailure: When the HTTP response does not have a 200 code.
|
||||
"""
|
||||
result = DiscoveryResult(uri)
|
||||
resp = fetchers.fetch(uri, headers={'Accept': YADIS_ACCEPT_HEADER})
|
||||
if resp.status not in (200, 206):
|
||||
raise DiscoveryFailure(
|
||||
'HTTP Response status from identity URL host is not 200. '
|
||||
'Got status %r' % (resp.status, ), resp)
|
||||
|
||||
# Note the URL after following redirects
|
||||
result.normalized_uri = resp.final_url
|
||||
|
||||
# Attempt to find out where to go to discover the document
|
||||
# or if we already have it
|
||||
result.content_type = resp.headers.get('content-type')
|
||||
|
||||
result.xrds_uri = whereIsYadis(resp)
|
||||
|
||||
if result.xrds_uri and result.usedYadisLocation():
|
||||
resp = fetchers.fetch(result.xrds_uri)
|
||||
if resp.status not in (200, 206):
|
||||
exc = DiscoveryFailure(
|
||||
'HTTP Response status from Yadis host is not 200. '
|
||||
'Got status %r' % (resp.status, ), resp)
|
||||
exc.identity_url = result.normalized_uri
|
||||
raise exc
|
||||
result.content_type = resp.headers.get('content-type')
|
||||
|
||||
result.response_text = resp.body
|
||||
return result
|
||||
|
||||
|
||||
def whereIsYadis(resp):
|
||||
"""Given a HTTPResponse, return the location of the Yadis document.
|
||||
|
||||
May be the URL just retrieved, another URL, or None if no suitable URL can
|
||||
be found.
|
||||
|
||||
[non-blocking]
|
||||
|
||||
@returns: str or None
|
||||
"""
|
||||
# Attempt to find out where to go to discover the document
|
||||
# or if we already have it
|
||||
content_type = resp.headers.get('content-type')
|
||||
|
||||
# According to the spec, the content-type header must be an exact
|
||||
# match, or else we have to look for an indirection.
|
||||
if (content_type and
|
||||
content_type.split(';', 1)[0].lower() == YADIS_CONTENT_TYPE):
|
||||
return resp.final_url
|
||||
else:
|
||||
# Try the header
|
||||
yadis_loc = resp.headers.get(YADIS_HEADER_NAME.lower())
|
||||
|
||||
if not yadis_loc:
|
||||
# Parse as HTML if the header is missing.
|
||||
#
|
||||
# XXX: do we want to do something with content-type, like
|
||||
# have a whitelist or a blacklist (for detecting that it's
|
||||
# HTML)?
|
||||
|
||||
# Decode body by encoding of file
|
||||
content_type = content_type or ''
|
||||
encoding = content_type.rsplit(';', 1)
|
||||
if (len(encoding) == 2 and
|
||||
encoding[1].strip().startswith('charset=')):
|
||||
encoding = encoding[1].split('=', 1)[1].strip()
|
||||
else:
|
||||
encoding = 'utf-8'
|
||||
|
||||
if isinstance(resp.body, bytes):
|
||||
try:
|
||||
content = resp.body.decode(encoding)
|
||||
except UnicodeError:
|
||||
# All right, the detected encoding has failed. Try with
|
||||
# UTF-8 (even if there was no detected encoding and we've
|
||||
# defaulted to UTF-8, it's not that expensive an operation)
|
||||
try:
|
||||
content = resp.body.decode('utf-8')
|
||||
except UnicodeError:
|
||||
# At this point the content cannot be decoded to a str
|
||||
# using the detected encoding or falling back to utf-8,
|
||||
# so we have to resort to replacing undecodable chars.
|
||||
# This *will* result in broken content but there isn't
|
||||
# anything else that can be done.
|
||||
content = resp.body.decode(encoding, 'replace')
|
||||
else:
|
||||
content = resp.body
|
||||
|
||||
try:
|
||||
yadis_loc = findHTMLMeta(StringIO(content))
|
||||
except (MetaNotFound, UnicodeError):
|
||||
# UnicodeError: Response body could not be encoded and xrds
|
||||
# location could not be found before troubles occur.
|
||||
pass
|
||||
|
||||
return yadis_loc
|
||||
@@ -0,0 +1,318 @@
|
||||
# -*- test-case-name: yadis.test.test_etxrd -*-
|
||||
"""
|
||||
ElementTree interface to an XRD document.
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
'nsTag',
|
||||
'mkXRDTag',
|
||||
'isXRDS',
|
||||
'parseXRDS',
|
||||
'getCanonicalID',
|
||||
'getYadisXRD',
|
||||
'getPriorityStrict',
|
||||
'getPriority',
|
||||
'prioSort',
|
||||
'iterServices',
|
||||
'expandService',
|
||||
'expandServices',
|
||||
]
|
||||
|
||||
import sys
|
||||
import random
|
||||
import functools
|
||||
|
||||
from datetime import datetime
|
||||
from time import strptime
|
||||
|
||||
from openid.oidutil import importElementTree, importSafeElementTree
|
||||
|
||||
ElementTree = importElementTree()
|
||||
SafeElementTree = importSafeElementTree()
|
||||
|
||||
from openid.yadis import xri
|
||||
|
||||
|
||||
class XRDSError(Exception):
|
||||
"""An error with the XRDS document."""
|
||||
|
||||
# The exception that triggered this exception
|
||||
reason = None
|
||||
|
||||
|
||||
class XRDSFraud(XRDSError):
|
||||
"""Raised when there's an assertion in the XRDS that it does not have
|
||||
the authority to make.
|
||||
"""
|
||||
|
||||
|
||||
def parseXRDS(text):
|
||||
"""Parse the given text as an XRDS document.
|
||||
|
||||
@return: ElementTree containing an XRDS document
|
||||
|
||||
@raises XRDSError: When there is a parse error or the document does
|
||||
not contain an XRDS.
|
||||
"""
|
||||
try:
|
||||
# lxml prefers to parse bytestrings, and occasionally chokes on a
|
||||
# combination of text strings and declared XML encodings -- see
|
||||
# https://github.com/necaris/python3-openid/issues/19
|
||||
# To avoid this, we ensure that the 'text' we're parsing is actually
|
||||
# a bytestring
|
||||
bytestring = text.encode('utf8') if isinstance(text, str) else text
|
||||
element = SafeElementTree.XML(bytestring)
|
||||
except (SystemExit, MemoryError, AssertionError, ImportError):
|
||||
raise
|
||||
except Exception as why:
|
||||
exc = XRDSError('Error parsing document as XML')
|
||||
exc.reason = why
|
||||
raise exc
|
||||
else:
|
||||
tree = ElementTree.ElementTree(element)
|
||||
if not isXRDS(tree):
|
||||
raise XRDSError('Not an XRDS document')
|
||||
|
||||
return tree
|
||||
|
||||
|
||||
XRD_NS_2_0 = 'xri://$xrd*($v*2.0)'
|
||||
XRDS_NS = 'xri://$xrds'
|
||||
|
||||
|
||||
def nsTag(ns, t):
|
||||
return '{%s}%s' % (ns, t)
|
||||
|
||||
|
||||
def mkXRDTag(t):
|
||||
"""basestring -> basestring
|
||||
|
||||
Create a tag name in the XRD 2.0 XML namespace suitable for using
|
||||
with ElementTree
|
||||
"""
|
||||
return nsTag(XRD_NS_2_0, t)
|
||||
|
||||
|
||||
def mkXRDSTag(t):
|
||||
"""basestring -> basestring
|
||||
|
||||
Create a tag name in the XRDS XML namespace suitable for using
|
||||
with ElementTree
|
||||
"""
|
||||
return nsTag(XRDS_NS, t)
|
||||
|
||||
|
||||
# Tags that are used in Yadis documents
|
||||
root_tag = mkXRDSTag('XRDS')
|
||||
service_tag = mkXRDTag('Service')
|
||||
xrd_tag = mkXRDTag('XRD')
|
||||
type_tag = mkXRDTag('Type')
|
||||
uri_tag = mkXRDTag('URI')
|
||||
expires_tag = mkXRDTag('Expires')
|
||||
|
||||
# Other XRD tags
|
||||
canonicalID_tag = mkXRDTag('CanonicalID')
|
||||
|
||||
|
||||
def isXRDS(xrd_tree):
|
||||
"""Is this document an XRDS document?"""
|
||||
root = xrd_tree.getroot()
|
||||
return root.tag == root_tag
|
||||
|
||||
|
||||
def getYadisXRD(xrd_tree):
|
||||
"""Return the XRD element that should contain the Yadis services"""
|
||||
xrd = None
|
||||
|
||||
# for the side-effect of assigning the last one in the list to the
|
||||
# xrd variable
|
||||
for xrd in xrd_tree.findall(xrd_tag):
|
||||
pass
|
||||
|
||||
# There were no elements found, or else xrd would be set to the
|
||||
# last one
|
||||
if xrd is None:
|
||||
raise XRDSError('No XRD present in tree')
|
||||
|
||||
return xrd
|
||||
|
||||
|
||||
def getXRDExpiration(xrd_element, default=None):
|
||||
"""Return the expiration date of this XRD element, or None if no
|
||||
expiration was specified.
|
||||
|
||||
@type xrd_element: ElementTree node
|
||||
|
||||
@param default: The value to use as the expiration if no
|
||||
expiration was specified in the XRD.
|
||||
|
||||
@rtype: datetime.datetime
|
||||
|
||||
@raises ValueError: If the xrd:Expires element is present, but its
|
||||
contents are not formatted according to the specification.
|
||||
"""
|
||||
expires_element = xrd_element.find(expires_tag)
|
||||
if expires_element is None:
|
||||
return default
|
||||
else:
|
||||
expires_string = expires_element.text
|
||||
|
||||
# Will raise ValueError if the string is not the expected format
|
||||
expires_time = strptime(expires_string, "%Y-%m-%dT%H:%M:%SZ")
|
||||
return datetime(*expires_time[0:6])
|
||||
|
||||
|
||||
def getCanonicalID(iname, xrd_tree):
|
||||
"""Return the CanonicalID from this XRDS document.
|
||||
|
||||
@param iname: the XRI being resolved.
|
||||
@type iname: unicode
|
||||
|
||||
@param xrd_tree: The XRDS output from the resolver.
|
||||
@type xrd_tree: ElementTree
|
||||
|
||||
@returns: The XRI CanonicalID or None.
|
||||
@returntype: unicode or None
|
||||
"""
|
||||
xrd_list = xrd_tree.findall(xrd_tag)
|
||||
xrd_list.reverse()
|
||||
|
||||
try:
|
||||
canonicalID = xri.XRI(xrd_list[0].findall(canonicalID_tag)[0].text)
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
childID = canonicalID.lower()
|
||||
|
||||
for xrd in xrd_list[1:]:
|
||||
parent_sought = childID.rsplit("!", 1)[0]
|
||||
parent = xri.XRI(xrd.findtext(canonicalID_tag))
|
||||
if parent_sought != parent.lower():
|
||||
raise XRDSFraud("%r can not come from %s" % (childID, parent))
|
||||
|
||||
childID = parent_sought
|
||||
|
||||
root = xri.rootAuthority(iname)
|
||||
if not xri.providerIsAuthoritative(root, childID):
|
||||
raise XRDSFraud("%r can not come from root %r" % (childID, root))
|
||||
|
||||
return canonicalID
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class _Max(object):
|
||||
"""
|
||||
Value that compares greater than any other value.
|
||||
|
||||
Should only be used as a singleton. Implemented for use as a
|
||||
priority value for when a priority is not specified.
|
||||
"""
|
||||
|
||||
def __lt__(self, other):
|
||||
return isinstance(other, self.__class__)
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, self.__class__)
|
||||
|
||||
|
||||
Max = _Max()
|
||||
|
||||
|
||||
def getPriorityStrict(element):
|
||||
"""Get the priority of this element.
|
||||
|
||||
Raises ValueError if the value of the priority is invalid. If no
|
||||
priority is specified, it returns a value that compares greater
|
||||
than any other value.
|
||||
"""
|
||||
prio_str = element.get('priority')
|
||||
if prio_str is not None:
|
||||
prio_val = int(prio_str)
|
||||
if prio_val >= 0:
|
||||
return prio_val
|
||||
else:
|
||||
raise ValueError('Priority values must be non-negative integers')
|
||||
|
||||
# Any errors in parsing the priority fall through to here
|
||||
return Max
|
||||
|
||||
|
||||
def getPriority(element):
|
||||
"""Get the priority of this element
|
||||
|
||||
Returns Max if no priority is specified or the priority value is invalid.
|
||||
"""
|
||||
try:
|
||||
return getPriorityStrict(element)
|
||||
except ValueError:
|
||||
return Max
|
||||
|
||||
|
||||
def prioSort(elements):
|
||||
"""Sort a list of elements that have priority attributes"""
|
||||
# Randomize the services before sorting so that equal priority
|
||||
# elements are load-balanced.
|
||||
random.shuffle(elements)
|
||||
|
||||
sorted_elems = sorted(elements, key=getPriority)
|
||||
return sorted_elems
|
||||
|
||||
|
||||
def iterServices(xrd_tree):
|
||||
"""Return an iterable over the Service elements in the Yadis XRD
|
||||
|
||||
sorted by priority"""
|
||||
xrd = getYadisXRD(xrd_tree)
|
||||
return prioSort(xrd.findall(service_tag))
|
||||
|
||||
|
||||
def sortedURIs(service_element):
|
||||
"""Given a Service element, return a list of the contents of all
|
||||
URI tags in priority order."""
|
||||
return [
|
||||
uri_element.text
|
||||
for uri_element in prioSort(service_element.findall(uri_tag))
|
||||
]
|
||||
|
||||
|
||||
def getTypeURIs(service_element):
|
||||
"""Given a Service element, return a list of the contents of all
|
||||
Type tags"""
|
||||
return [
|
||||
type_element.text for type_element in service_element.findall(type_tag)
|
||||
]
|
||||
|
||||
|
||||
def expandService(service_element):
|
||||
"""Take a service element and expand it into an iterator of:
|
||||
([type_uri], uri, service_element)
|
||||
"""
|
||||
uris = sortedURIs(service_element)
|
||||
if not uris:
|
||||
uris = [None]
|
||||
|
||||
expanded = []
|
||||
for uri in uris:
|
||||
type_uris = getTypeURIs(service_element)
|
||||
expanded.append((type_uris, uri, service_element))
|
||||
|
||||
return expanded
|
||||
|
||||
|
||||
def expandServices(service_elements):
|
||||
"""Take a sorted iterator of service elements and expand it into a
|
||||
sorted iterator of:
|
||||
([type_uri], uri, service_element)
|
||||
|
||||
There may be more than one item in the resulting list for each
|
||||
service element if there is more than one URI or type for a
|
||||
service, but each triple will be unique.
|
||||
|
||||
If there is no URI or Type for a Service element, it will not
|
||||
appear in the result.
|
||||
"""
|
||||
expanded = []
|
||||
for service_element in service_elements:
|
||||
expanded.extend(expandService(service_element))
|
||||
|
||||
return expanded
|
||||
@@ -0,0 +1,210 @@
|
||||
"""This module contains functions and classes used for extracting
|
||||
endpoint information out of a Yadis XRD file using the ElementTree XML
|
||||
parser.
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
'BasicServiceEndpoint',
|
||||
'mkFilter',
|
||||
'IFilter',
|
||||
'TransformFilterMaker',
|
||||
'CompoundFilter',
|
||||
]
|
||||
|
||||
from openid.yadis.etxrd import expandService
|
||||
import collections
|
||||
|
||||
|
||||
class BasicServiceEndpoint(object):
|
||||
"""Generic endpoint object that contains parsed service
|
||||
information, as well as a reference to the service element from
|
||||
which it was generated. If there is more than one xrd:Type or
|
||||
xrd:URI in the xrd:Service, this object represents just one of
|
||||
those pairs.
|
||||
|
||||
This object can be used as a filter, because it implements
|
||||
fromBasicServiceEndpoint.
|
||||
|
||||
The simplest kind of filter you can write implements
|
||||
fromBasicServiceEndpoint, which takes one of these objects.
|
||||
"""
|
||||
|
||||
def __init__(self, yadis_url, type_uris, uri, service_element):
|
||||
self.type_uris = type_uris
|
||||
self.yadis_url = yadis_url
|
||||
self.uri = uri
|
||||
self.service_element = service_element
|
||||
|
||||
def matchTypes(self, type_uris):
|
||||
"""Query this endpoint to see if it has any of the given type
|
||||
URIs. This is useful for implementing other endpoint classes
|
||||
that e.g. need to check for the presence of multiple versions
|
||||
of a single protocol.
|
||||
|
||||
@param type_uris: The URIs that you wish to check
|
||||
@type type_uris: iterable of str
|
||||
|
||||
@return: all types that are in both in type_uris and
|
||||
self.type_uris
|
||||
"""
|
||||
return [uri for uri in type_uris if uri in self.type_uris]
|
||||
|
||||
def fromBasicServiceEndpoint(endpoint):
|
||||
"""Trivial transform from a basic endpoint to itself. This
|
||||
method exists to allow BasicServiceEndpoint to be used as a
|
||||
filter.
|
||||
|
||||
If you are subclassing this object, re-implement this function.
|
||||
|
||||
@param endpoint: An instance of BasicServiceEndpoint
|
||||
@return: The object that was passed in, with no processing.
|
||||
"""
|
||||
return endpoint
|
||||
|
||||
fromBasicServiceEndpoint = staticmethod(fromBasicServiceEndpoint)
|
||||
|
||||
|
||||
class IFilter(object):
|
||||
"""Interface for Yadis filter objects. Other filter-like things
|
||||
are convertable to this class."""
|
||||
|
||||
def getServiceEndpoints(self, yadis_url, service_element):
|
||||
"""Returns an iterator of endpoint objects"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class TransformFilterMaker(object):
|
||||
"""Take a list of basic filters and makes a filter that transforms
|
||||
the basic filter into a top-level filter. This is mostly useful
|
||||
for the implementation of mkFilter, which should only be needed
|
||||
for special cases or internal use by this library.
|
||||
|
||||
This object is useful for creating simple filters for services
|
||||
that use one URI and are specified by one Type (we expect most
|
||||
Types will fit this paradigm).
|
||||
|
||||
Creates a BasicServiceEndpoint object and apply the filter
|
||||
functions to it until one of them returns a value.
|
||||
"""
|
||||
|
||||
def __init__(self, filter_functions):
|
||||
"""Initialize the filter maker's state
|
||||
|
||||
@param filter_functions: The endpoint transformer functions to
|
||||
apply to the basic endpoint. These are called in turn
|
||||
until one of them does not return None, and the result of
|
||||
that transformer is returned.
|
||||
"""
|
||||
self.filter_functions = filter_functions
|
||||
|
||||
def getServiceEndpoints(self, yadis_url, service_element):
|
||||
"""Returns an iterator of endpoint objects produced by the
|
||||
filter functions."""
|
||||
endpoints = []
|
||||
|
||||
# Do an expansion of the service element by xrd:Type and xrd:URI
|
||||
for type_uris, uri, _ in expandService(service_element):
|
||||
|
||||
# Create a basic endpoint object to represent this
|
||||
# yadis_url, Service, Type, URI combination
|
||||
endpoint = BasicServiceEndpoint(yadis_url, type_uris, uri,
|
||||
service_element)
|
||||
|
||||
e = self.applyFilters(endpoint)
|
||||
if e is not None:
|
||||
endpoints.append(e)
|
||||
|
||||
return endpoints
|
||||
|
||||
def applyFilters(self, endpoint):
|
||||
"""Apply filter functions to an endpoint until one of them
|
||||
returns non-None."""
|
||||
for filter_function in self.filter_functions:
|
||||
e = filter_function(endpoint)
|
||||
if e is not None:
|
||||
# Once one of the filters has returned an
|
||||
# endpoint, do not apply any more.
|
||||
return e
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class CompoundFilter(object):
|
||||
"""Create a new filter that applies a set of filters to an endpoint
|
||||
and collects their results.
|
||||
"""
|
||||
|
||||
def __init__(self, subfilters):
|
||||
self.subfilters = subfilters
|
||||
|
||||
def getServiceEndpoints(self, yadis_url, service_element):
|
||||
"""Generate all endpoint objects for all of the subfilters of
|
||||
this filter and return their concatenation."""
|
||||
endpoints = []
|
||||
for subfilter in self.subfilters:
|
||||
endpoints.extend(
|
||||
subfilter.getServiceEndpoints(yadis_url, service_element))
|
||||
return endpoints
|
||||
|
||||
|
||||
# Exception raised when something is not able to be turned into a filter
|
||||
filter_type_error = TypeError(
|
||||
'Expected a filter, an endpoint, a callable or a list of any of these.')
|
||||
|
||||
|
||||
def mkFilter(parts):
|
||||
"""Convert a filter-convertable thing into a filter
|
||||
|
||||
@param parts: a filter, an endpoint, a callable, or a list of any of these.
|
||||
"""
|
||||
# Convert the parts into a list, and pass to mkCompoundFilter
|
||||
if parts is None:
|
||||
parts = [BasicServiceEndpoint]
|
||||
|
||||
try:
|
||||
parts = list(parts)
|
||||
except TypeError:
|
||||
return mkCompoundFilter([parts])
|
||||
else:
|
||||
return mkCompoundFilter(parts)
|
||||
|
||||
|
||||
def mkCompoundFilter(parts):
|
||||
"""Create a filter out of a list of filter-like things
|
||||
|
||||
Used by mkFilter
|
||||
|
||||
@param parts: list of filter, endpoint, callable or list of any of these
|
||||
"""
|
||||
# Separate into a list of callables and a list of filter objects
|
||||
transformers = []
|
||||
filters = []
|
||||
for subfilter in parts:
|
||||
try:
|
||||
subfilter = list(subfilter)
|
||||
except TypeError:
|
||||
# If it's not an iterable
|
||||
if hasattr(subfilter, 'getServiceEndpoints'):
|
||||
# It's a full filter
|
||||
filters.append(subfilter)
|
||||
elif hasattr(subfilter, 'fromBasicServiceEndpoint'):
|
||||
# It's an endpoint object, so put its endpoint
|
||||
# conversion attribute into the list of endpoint
|
||||
# transformers
|
||||
transformers.append(subfilter.fromBasicServiceEndpoint)
|
||||
elif isinstance(subfilter, collections.Callable):
|
||||
# It's a simple callable, so add it to the list of
|
||||
# endpoint transformers
|
||||
transformers.append(subfilter)
|
||||
else:
|
||||
raise filter_type_error
|
||||
else:
|
||||
filters.append(mkCompoundFilter(subfilter))
|
||||
|
||||
if transformers:
|
||||
filters.append(TransformFilterMaker(transformers))
|
||||
|
||||
if len(filters) == 1:
|
||||
return filters[0]
|
||||
else:
|
||||
return CompoundFilter(filters)
|
||||
@@ -0,0 +1,195 @@
|
||||
class YadisServiceManager(object):
|
||||
"""Holds the state of a list of selected Yadis services, managing
|
||||
storing it in a session and iterating over the services in order."""
|
||||
|
||||
def __init__(self, starting_url, yadis_url, services, session_key):
|
||||
# The URL that was used to initiate the Yadis protocol
|
||||
self.starting_url = starting_url
|
||||
|
||||
# The URL after following redirects (the identifier)
|
||||
self.yadis_url = yadis_url
|
||||
|
||||
# List of service elements
|
||||
self.services = list(services)
|
||||
|
||||
self.session_key = session_key
|
||||
|
||||
# Reference to the current service object
|
||||
self._current = None
|
||||
|
||||
def __len__(self):
|
||||
"""How many untried services remain?"""
|
||||
return len(self.services)
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
"""Return the next service
|
||||
|
||||
self.current() will continue to return that service until the
|
||||
next call to this method."""
|
||||
try:
|
||||
self._current = self.services.pop(0)
|
||||
except IndexError:
|
||||
raise StopIteration
|
||||
else:
|
||||
return self._current
|
||||
|
||||
def current(self):
|
||||
"""Return the current service.
|
||||
|
||||
Returns None if there are no services left.
|
||||
"""
|
||||
return self._current
|
||||
|
||||
def forURL(self, url):
|
||||
return url in [self.starting_url, self.yadis_url]
|
||||
|
||||
def started(self):
|
||||
"""Has the first service been returned?"""
|
||||
return self._current is not None
|
||||
|
||||
def store(self, session):
|
||||
"""Store this object in the session, by its session key."""
|
||||
session[self.session_key] = self
|
||||
|
||||
|
||||
class Discovery(object):
|
||||
"""State management for discovery.
|
||||
|
||||
High-level usage pattern is to call .getNextService(discover) in
|
||||
order to find the next available service for this user for this
|
||||
session. Once a request completes, call .finish() to clean up the
|
||||
session state.
|
||||
|
||||
@ivar session: a dict-like object that stores state unique to the
|
||||
requesting user-agent. This object must be able to store
|
||||
serializable objects.
|
||||
|
||||
@ivar url: the URL that is used to make the discovery request
|
||||
|
||||
@ivar session_key_suffix: The suffix that will be used to identify
|
||||
this object in the session object.
|
||||
"""
|
||||
|
||||
DEFAULT_SUFFIX = 'auth'
|
||||
PREFIX = '_yadis_services_'
|
||||
|
||||
def __init__(self, session, url, session_key_suffix=None):
|
||||
"""Initialize a discovery object"""
|
||||
self.session = session
|
||||
self.url = url
|
||||
if session_key_suffix is None:
|
||||
session_key_suffix = self.DEFAULT_SUFFIX
|
||||
|
||||
self.session_key_suffix = session_key_suffix
|
||||
|
||||
def getNextService(self, discover):
|
||||
"""Return the next authentication service for the pair of
|
||||
user_input and session. This function handles fallback.
|
||||
|
||||
|
||||
@param discover: a callable that takes a URL and returns a
|
||||
list of services
|
||||
|
||||
@type discover: str -> [service]
|
||||
|
||||
|
||||
@return: the next available service
|
||||
"""
|
||||
manager = self.getManager()
|
||||
if manager is not None and not manager:
|
||||
self.destroyManager()
|
||||
|
||||
if not manager:
|
||||
yadis_url, services = discover(self.url)
|
||||
manager = self.createManager(services, yadis_url)
|
||||
|
||||
if manager:
|
||||
service = next(manager)
|
||||
manager.store(self.session)
|
||||
else:
|
||||
service = None
|
||||
|
||||
return service
|
||||
|
||||
def cleanup(self, force=False):
|
||||
"""Clean up Yadis-related services in the session and return
|
||||
the most-recently-attempted service from the manager, if one
|
||||
exists.
|
||||
|
||||
@param force: True if the manager should be deleted regardless
|
||||
of whether it's a manager for self.url.
|
||||
|
||||
@return: current service endpoint object or None if there is
|
||||
no current service
|
||||
"""
|
||||
manager = self.getManager(force=force)
|
||||
if manager is not None:
|
||||
service = manager.current()
|
||||
self.destroyManager(force=force)
|
||||
else:
|
||||
service = None
|
||||
|
||||
return service
|
||||
|
||||
### Lower-level methods
|
||||
|
||||
def getSessionKey(self):
|
||||
"""Get the session key for this starting URL and suffix
|
||||
|
||||
@return: The session key
|
||||
@rtype: str
|
||||
"""
|
||||
return self.PREFIX + self.session_key_suffix
|
||||
|
||||
def getManager(self, force=False):
|
||||
"""Extract the YadisServiceManager for this object's URL and
|
||||
suffix from the session.
|
||||
|
||||
@param force: True if the manager should be returned
|
||||
regardless of whether it's a manager for self.url.
|
||||
|
||||
@return: The current YadisServiceManager, if it's for this
|
||||
URL, or else None
|
||||
"""
|
||||
manager = self.session.get(self.getSessionKey())
|
||||
if (manager is not None and (manager.forURL(self.url) or force)):
|
||||
return manager
|
||||
else:
|
||||
return None
|
||||
|
||||
def createManager(self, services, yadis_url=None):
|
||||
"""Create a new YadisService Manager for this starting URL and
|
||||
suffix, and store it in the session.
|
||||
|
||||
@raises KeyError: When I already have a manager.
|
||||
|
||||
@return: A new YadisServiceManager or None
|
||||
"""
|
||||
key = self.getSessionKey()
|
||||
if self.getManager():
|
||||
raise KeyError('There is already a %r manager for %r' %
|
||||
(key, self.url))
|
||||
|
||||
if not services:
|
||||
return None
|
||||
|
||||
manager = YadisServiceManager(self.url, yadis_url, services, key)
|
||||
manager.store(self.session)
|
||||
return manager
|
||||
|
||||
def destroyManager(self, force=False):
|
||||
"""Delete any YadisServiceManager with this starting URL and
|
||||
suffix from the session.
|
||||
|
||||
If there is no service manager or the service manager is for a
|
||||
different URL, it silently does nothing.
|
||||
|
||||
@param force: True if the manager should be deleted regardless
|
||||
of whether it's a manager for self.url.
|
||||
"""
|
||||
if self.getManager(force=force) is not None:
|
||||
key = self.getSessionKey()
|
||||
del self.session[key]
|
||||
@@ -0,0 +1,207 @@
|
||||
__all__ = ['findHTMLMeta', 'MetaNotFound']
|
||||
|
||||
from html.parser import HTMLParser
|
||||
import html.entities
|
||||
import re
|
||||
import sys
|
||||
|
||||
from openid.yadis.constants import YADIS_HEADER_NAME
|
||||
|
||||
# Size of the chunks to search at a time (also the amount that gets
|
||||
# read at a time)
|
||||
CHUNK_SIZE = 1024 * 16 # 16 KB
|
||||
|
||||
|
||||
class ParseDone(Exception):
|
||||
"""Exception to hold the URI that was located when the parse is
|
||||
finished. If the parse finishes without finding the URI, set it to
|
||||
None."""
|
||||
|
||||
|
||||
class MetaNotFound(Exception):
|
||||
"""Exception to hold the content of the page if we did not find
|
||||
the appropriate <meta> tag"""
|
||||
|
||||
|
||||
re_flags = re.IGNORECASE | re.UNICODE | re.VERBOSE
|
||||
ent_pat = r'''
|
||||
&
|
||||
|
||||
(?: \#x (?P<hex> [a-f0-9]+ )
|
||||
| \# (?P<dec> \d+ )
|
||||
| (?P<word> \w+ )
|
||||
)
|
||||
|
||||
;'''
|
||||
|
||||
ent_re = re.compile(ent_pat, re_flags)
|
||||
|
||||
|
||||
def substituteMO(mo):
|
||||
if mo.lastgroup == 'hex':
|
||||
codepoint = int(mo.group('hex'), 16)
|
||||
elif mo.lastgroup == 'dec':
|
||||
codepoint = int(mo.group('dec'))
|
||||
else:
|
||||
assert mo.lastgroup == 'word'
|
||||
codepoint = html.entities.name2codepoint.get(mo.group('word'))
|
||||
|
||||
if codepoint is None:
|
||||
return mo.group()
|
||||
else:
|
||||
return chr(codepoint)
|
||||
|
||||
|
||||
def substituteEntities(s):
|
||||
return ent_re.sub(substituteMO, s)
|
||||
|
||||
|
||||
class YadisHTMLParser(HTMLParser):
|
||||
"""Parser that finds a meta http-equiv tag in the head of a html
|
||||
document.
|
||||
|
||||
When feeding in data, if the tag is matched or it will never be
|
||||
found, the parser will raise ParseDone with the uri as the first
|
||||
attribute.
|
||||
|
||||
Parsing state diagram
|
||||
=====================
|
||||
|
||||
Any unlisted input does not affect the state::
|
||||
|
||||
1, 2, 5 8
|
||||
+--------------------------+ +-+
|
||||
| | | |
|
||||
4 | 3 1, 2, 5, 7 v | v
|
||||
TOP -> HTML -> HEAD ----------> TERMINATED
|
||||
| | ^ | ^ ^
|
||||
| | 3 | | | |
|
||||
| +------------+ +-> FOUND ------+ |
|
||||
| 6 8 |
|
||||
| 1, 2 |
|
||||
+------------------------------------+
|
||||
|
||||
1. any of </body>, </html>, </head> -> TERMINATE
|
||||
2. <body> -> TERMINATE
|
||||
3. <head> -> HEAD
|
||||
4. <html> -> HTML
|
||||
5. <html> -> TERMINATE
|
||||
6. <meta http-equiv='X-XRDS-Location'> -> FOUND
|
||||
7. <head> -> TERMINATE
|
||||
8. Any input -> TERMINATE
|
||||
"""
|
||||
TOP = 0
|
||||
HTML = 1
|
||||
HEAD = 2
|
||||
FOUND = 3
|
||||
TERMINATED = 4
|
||||
|
||||
def __init__(self):
|
||||
if (sys.version_info.minor <= 2):
|
||||
# Python 3.2 and below actually require the `strict` argument
|
||||
# to `html.parser.HTMLParser` -- otherwise it's deprecated and
|
||||
# we don't want to pass it
|
||||
super(YadisHTMLParser, self).__init__(strict=False)
|
||||
else:
|
||||
super(YadisHTMLParser, self).__init__()
|
||||
self.phase = self.TOP
|
||||
|
||||
def _terminate(self):
|
||||
self.phase = self.TERMINATED
|
||||
raise ParseDone(None)
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
# If we ever see an end of head, body, or html, bail out right away.
|
||||
# [1]
|
||||
if tag in ['head', 'body', 'html']:
|
||||
self._terminate()
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
# if we ever see a start body tag, bail out right away, since
|
||||
# we want to prevent the meta tag from appearing in the body
|
||||
# [2]
|
||||
if tag == 'body':
|
||||
self._terminate()
|
||||
|
||||
if self.phase == self.TOP:
|
||||
# At the top level, allow a html tag or a head tag to move
|
||||
# to the head or html phase
|
||||
if tag == 'head':
|
||||
# [3]
|
||||
self.phase = self.HEAD
|
||||
elif tag == 'html':
|
||||
# [4]
|
||||
self.phase = self.HTML
|
||||
|
||||
elif self.phase == self.HTML:
|
||||
# if we are in the html tag, allow a head tag to move to
|
||||
# the HEAD phase. If we get another html tag, then bail
|
||||
# out
|
||||
if tag == 'head':
|
||||
# [3]
|
||||
self.phase = self.HEAD
|
||||
elif tag == 'html':
|
||||
# [5]
|
||||
self._terminate()
|
||||
|
||||
elif self.phase == self.HEAD:
|
||||
# If we are in the head phase, look for the appropriate
|
||||
# meta tag. If we get a head or body tag, bail out.
|
||||
if tag == 'meta':
|
||||
attrs_d = dict(attrs)
|
||||
http_equiv = attrs_d.get('http-equiv', '').lower()
|
||||
if http_equiv == YADIS_HEADER_NAME.lower():
|
||||
raw_attr = attrs_d.get('content')
|
||||
yadis_loc = substituteEntities(raw_attr)
|
||||
# [6]
|
||||
self.phase = self.FOUND
|
||||
raise ParseDone(yadis_loc)
|
||||
|
||||
elif tag in ('head', 'html'):
|
||||
# [5], [7]
|
||||
self._terminate()
|
||||
|
||||
def feed(self, chars):
|
||||
# [8]
|
||||
if self.phase in (self.TERMINATED, self.FOUND):
|
||||
self._terminate()
|
||||
|
||||
return super(YadisHTMLParser, self).feed(chars)
|
||||
|
||||
|
||||
def findHTMLMeta(stream):
|
||||
"""Look for a meta http-equiv tag with the YADIS header name.
|
||||
|
||||
@param stream: Source of the html text
|
||||
@type stream: Object that implements a read() method that works
|
||||
like file.read
|
||||
|
||||
@return: The URI from which to fetch the XRDS document
|
||||
@rtype: str
|
||||
|
||||
@raises MetaNotFound: raised with the content that was
|
||||
searched as the first parameter.
|
||||
"""
|
||||
parser = YadisHTMLParser()
|
||||
chunks = []
|
||||
|
||||
while 1:
|
||||
chunk = stream.read(CHUNK_SIZE)
|
||||
if not chunk:
|
||||
# End of file
|
||||
break
|
||||
|
||||
chunks.append(chunk)
|
||||
try:
|
||||
parser.feed(chunk)
|
||||
except ParseDone as why:
|
||||
uri = why.args[0]
|
||||
if uri is None:
|
||||
# Parse finished, but we may need the rest of the file
|
||||
chunks.append(stream.read())
|
||||
break
|
||||
else:
|
||||
return uri
|
||||
|
||||
content = ''.join(chunks)
|
||||
raise MetaNotFound(content)
|
||||
@@ -0,0 +1,56 @@
|
||||
# -*- test-case-name: openid.test.test_services -*-
|
||||
|
||||
from openid.yadis.filters import mkFilter
|
||||
from openid.yadis.discover import discover, DiscoveryFailure
|
||||
from openid.yadis.etxrd import parseXRDS, iterServices, XRDSError
|
||||
|
||||
|
||||
def getServiceEndpoints(input_url, flt=None):
|
||||
"""Perform the Yadis protocol on the input URL and return an
|
||||
iterable of resulting endpoint objects.
|
||||
|
||||
@param flt: A filter object or something that is convertable to
|
||||
a filter object (using mkFilter) that will be used to generate
|
||||
endpoint objects. This defaults to generating BasicEndpoint
|
||||
objects.
|
||||
|
||||
@param input_url: The URL on which to perform the Yadis protocol
|
||||
|
||||
@return: The normalized identity URL and an iterable of endpoint
|
||||
objects generated by the filter function.
|
||||
|
||||
@rtype: (str, [endpoint])
|
||||
|
||||
@raises DiscoveryFailure: when Yadis fails to obtain an XRDS document.
|
||||
"""
|
||||
result = discover(input_url)
|
||||
try:
|
||||
endpoints = applyFilter(result.normalized_uri, result.response_text,
|
||||
flt)
|
||||
except XRDSError as err:
|
||||
raise DiscoveryFailure(str(err), None)
|
||||
return (result.normalized_uri, endpoints)
|
||||
|
||||
|
||||
def applyFilter(normalized_uri, xrd_data, flt=None):
|
||||
"""Generate an iterable of endpoint objects given this input data,
|
||||
presumably from the result of performing the Yadis protocol.
|
||||
|
||||
@param normalized_uri: The input URL, after following redirects,
|
||||
as in the Yadis protocol.
|
||||
|
||||
|
||||
@param xrd_data: The XML text the XRDS file fetched from the
|
||||
normalized URI.
|
||||
@type xrd_data: str
|
||||
|
||||
"""
|
||||
flt = mkFilter(flt)
|
||||
et = parseXRDS(xrd_data)
|
||||
|
||||
endpoints = []
|
||||
for service_element in iterServices(et):
|
||||
endpoints.extend(
|
||||
flt.getServiceEndpoints(normalized_uri, service_element))
|
||||
|
||||
return endpoints
|
||||
@@ -0,0 +1,122 @@
|
||||
# -*- test-case-name: openid.test.test_xri -*-
|
||||
"""Utility functions for handling XRIs.
|
||||
|
||||
@see: XRI Syntax v2.0 at the U{OASIS XRI Technical Committee<http://www.oasis-open.org/committees/tc_home.php?wg_abbrev=xri>}
|
||||
"""
|
||||
|
||||
import re
|
||||
from functools import reduce
|
||||
|
||||
from openid import codecutil # registers 'oid_percent_escape' encoding handler
|
||||
|
||||
XRI_AUTHORITIES = ['!', '=', '@', '+', '$', '(']
|
||||
|
||||
|
||||
def identifierScheme(identifier):
|
||||
"""Determine if this identifier is an XRI or URI.
|
||||
|
||||
@returns: C{"XRI"} or C{"URI"}
|
||||
"""
|
||||
if identifier.startswith('xri://') or (identifier and
|
||||
identifier[0] in XRI_AUTHORITIES):
|
||||
return "XRI"
|
||||
else:
|
||||
return "URI"
|
||||
|
||||
|
||||
def toIRINormal(xri):
|
||||
"""Transform an XRI to IRI-normal form."""
|
||||
if not xri.startswith('xri://'):
|
||||
xri = 'xri://' + xri
|
||||
return escapeForIRI(xri)
|
||||
|
||||
|
||||
_xref_re = re.compile('\((.*?)\)')
|
||||
|
||||
|
||||
def _escape_xref(xref_match):
|
||||
"""Escape things that need to be escaped if they're in a cross-reference.
|
||||
"""
|
||||
xref = xref_match.group()
|
||||
xref = xref.replace('/', '%2F')
|
||||
xref = xref.replace('?', '%3F')
|
||||
xref = xref.replace('#', '%23')
|
||||
return xref
|
||||
|
||||
|
||||
def escapeForIRI(xri):
|
||||
"""Escape things that need to be escaped when transforming to an IRI."""
|
||||
xri = xri.replace('%', '%25')
|
||||
xri = _xref_re.sub(_escape_xref, xri)
|
||||
return xri
|
||||
|
||||
|
||||
def toURINormal(xri):
|
||||
"""Transform an XRI to URI normal form."""
|
||||
return iriToURI(toIRINormal(xri))
|
||||
|
||||
|
||||
def iriToURI(iri):
|
||||
"""Transform an IRI to a URI by escaping unicode."""
|
||||
# According to RFC 3987, section 3.1, "Mapping of IRIs to URIs"
|
||||
if isinstance(iri, bytes):
|
||||
iri = str(iri, encoding="utf-8")
|
||||
return iri.encode('ascii', errors='oid_percent_escape').decode()
|
||||
|
||||
|
||||
def providerIsAuthoritative(providerID, canonicalID):
|
||||
"""Is this provider ID authoritative for this XRI?
|
||||
|
||||
@returntype: bool
|
||||
"""
|
||||
# XXX: can't use rsplit until we require python >= 2.4.
|
||||
lastbang = canonicalID.rindex('!')
|
||||
parent = canonicalID[:lastbang]
|
||||
return parent == providerID
|
||||
|
||||
|
||||
def rootAuthority(xri):
|
||||
"""Return the root authority for an XRI.
|
||||
|
||||
Example::
|
||||
|
||||
rootAuthority("xri://@example") == "xri://@"
|
||||
|
||||
@type xri: unicode
|
||||
@returntype: unicode
|
||||
"""
|
||||
if xri.startswith('xri://'):
|
||||
xri = xri[6:]
|
||||
authority = xri.split('/', 1)[0]
|
||||
if authority[0] == '(':
|
||||
# Cross-reference.
|
||||
# XXX: This is incorrect if someone nests cross-references so there
|
||||
# is another close-paren in there. Hopefully nobody does that
|
||||
# before we have a real xriparse function. Hopefully nobody does
|
||||
# that *ever*.
|
||||
root = authority[:authority.index(')') + 1]
|
||||
elif authority[0] in XRI_AUTHORITIES:
|
||||
# Other XRI reference.
|
||||
root = authority[0]
|
||||
else:
|
||||
# IRI reference. XXX: Can IRI authorities have segments?
|
||||
segments = authority.split('!')
|
||||
segments = reduce(list.__add__, [s.split('*') for s in segments])
|
||||
root = segments[0]
|
||||
|
||||
return XRI(root)
|
||||
|
||||
|
||||
def XRI(xri):
|
||||
"""An XRI object allowing comparison of XRI.
|
||||
|
||||
Ideally, this would do full normalization and provide comparsion
|
||||
operators as per XRI Syntax. Right now, it just does a bit of
|
||||
canonicalization by ensuring the xri scheme is present.
|
||||
|
||||
@param xri: an xri string
|
||||
@type xri: unicode
|
||||
"""
|
||||
if not xri.startswith('xri://'):
|
||||
xri = 'xri://' + xri
|
||||
return xri
|
||||
@@ -0,0 +1,123 @@
|
||||
# -*- test-case-name: openid.test.test_xrires -*-
|
||||
"""XRI resolution.
|
||||
"""
|
||||
|
||||
from urllib.parse import urlencode
|
||||
from openid import fetchers
|
||||
from openid.yadis import etxrd
|
||||
from openid.yadis.xri import toURINormal
|
||||
from openid.yadis.services import iterServices
|
||||
|
||||
DEFAULT_PROXY = 'http://proxy.xri.net/'
|
||||
|
||||
|
||||
class ProxyResolver(object):
|
||||
"""Python interface to a remote XRI proxy resolver.
|
||||
"""
|
||||
|
||||
def __init__(self, proxy_url=DEFAULT_PROXY):
|
||||
self.proxy_url = proxy_url
|
||||
|
||||
def queryURL(self, xri, service_type=None):
|
||||
"""Build a URL to query the proxy resolver.
|
||||
|
||||
@param xri: An XRI to resolve.
|
||||
@type xri: unicode
|
||||
|
||||
@param service_type: The service type to resolve, if you desire
|
||||
service endpoint selection. A service type is a URI.
|
||||
@type service_type: str
|
||||
|
||||
@returns: a URL
|
||||
@returntype: str
|
||||
"""
|
||||
# Trim off the xri:// prefix. The proxy resolver didn't accept it
|
||||
# when this code was written, but that may (or may not) change for
|
||||
# XRI Resolution 2.0 Working Draft 11.
|
||||
qxri = toURINormal(xri)[6:]
|
||||
hxri = self.proxy_url + qxri
|
||||
args = {
|
||||
# XXX: If the proxy resolver will ensure that it doesn't return
|
||||
# bogus CanonicalIDs (as per Steve's message of 15 Aug 2006
|
||||
# 11:13:42), then we could ask for application/xrd+xml instead,
|
||||
# which would give us a bit less to process.
|
||||
'_xrd_r': 'application/xrds+xml',
|
||||
}
|
||||
if service_type:
|
||||
args['_xrd_t'] = service_type
|
||||
else:
|
||||
# Don't perform service endpoint selection.
|
||||
args['_xrd_r'] += ';sep=false'
|
||||
query = _appendArgs(hxri, args)
|
||||
return query
|
||||
|
||||
def query(self, xri, service_types):
|
||||
"""Resolve some services for an XRI.
|
||||
|
||||
Note: I don't implement any service endpoint selection beyond what
|
||||
the resolver I'm querying does, so the Services I return may well
|
||||
include Services that were not of the types you asked for.
|
||||
|
||||
May raise fetchers.HTTPFetchingError or L{etxrd.XRDSError} if
|
||||
the fetching or parsing don't go so well.
|
||||
|
||||
@param xri: An XRI to resolve.
|
||||
@type xri: unicode
|
||||
|
||||
@param service_types: A list of services types to query for. Service
|
||||
types are URIs.
|
||||
@type service_types: list of str
|
||||
|
||||
@returns: tuple of (CanonicalID, Service elements)
|
||||
@returntype: (unicode, list of C{ElementTree.Element}s)
|
||||
"""
|
||||
# FIXME: No test coverage!
|
||||
services = []
|
||||
# Make a seperate request to the proxy resolver for each service
|
||||
# type, as, if it is following Refs, it could return a different
|
||||
# XRDS for each.
|
||||
|
||||
canonicalID = None
|
||||
|
||||
for service_type in service_types:
|
||||
url = self.queryURL(xri, service_type)
|
||||
response = fetchers.fetch(url)
|
||||
if response.status not in (200, 206):
|
||||
# XXX: sucks to fail silently.
|
||||
# print "response not OK:", response
|
||||
continue
|
||||
et = etxrd.parseXRDS(response.body)
|
||||
canonicalID = etxrd.getCanonicalID(xri, et)
|
||||
some_services = list(iterServices(et))
|
||||
services.extend(some_services)
|
||||
# TODO:
|
||||
# * If we do get hits for multiple service_types, we're almost
|
||||
# certainly going to have duplicated service entries and
|
||||
# broken priority ordering.
|
||||
return canonicalID, services
|
||||
|
||||
|
||||
def _appendArgs(url, args):
|
||||
"""Append some arguments to an HTTP query.
|
||||
"""
|
||||
# to be merged with oidutil.appendArgs when we combine the projects.
|
||||
if hasattr(args, 'items'):
|
||||
args = list(args.items())
|
||||
args.sort()
|
||||
|
||||
if len(args) == 0:
|
||||
return url
|
||||
|
||||
# According to XRI Resolution section "QXRI query parameters":
|
||||
#
|
||||
# """If the original QXRI had a null query component (only a leading
|
||||
# question mark), or a query component consisting of only question
|
||||
# marks, one additional leading question mark MUST be added when
|
||||
# adding any XRI resolution parameters."""
|
||||
|
||||
if '?' in url.rstrip('?'):
|
||||
sep = '&'
|
||||
else:
|
||||
sep = '?'
|
||||
|
||||
return '%s%s%s' % (url, sep, urlencode(args))
|
||||
Reference in New Issue
Block a user