etxrd.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. # -*- test-case-name: yadis.test.test_etxrd -*-
  2. """
  3. ElementTree interface to an XRD document.
  4. """
  5. __all__ = [
  6. 'nsTag',
  7. 'mkXRDTag',
  8. 'isXRDS',
  9. 'parseXRDS',
  10. 'getCanonicalID',
  11. 'getYadisXRD',
  12. 'getPriorityStrict',
  13. 'getPriority',
  14. 'prioSort',
  15. 'iterServices',
  16. 'expandService',
  17. 'expandServices',
  18. ]
  19. import sys
  20. import random
  21. from datetime import datetime
  22. from time import strptime
  23. from openid.oidutil import importElementTree
  24. ElementTree = importElementTree()
  25. # the different elementtree modules don't have a common exception
  26. # model. We just want to be able to catch the exceptions that signify
  27. # malformed XML data and wrap them, so that the other library code
  28. # doesn't have to know which XML library we're using.
  29. try:
  30. # Make the parser raise an exception so we can sniff out the type
  31. # of exceptions
  32. ElementTree.XML('> purposely malformed XML <')
  33. except (SystemExit, MemoryError, AssertionError, ImportError):
  34. raise
  35. except:
  36. XMLError = sys.exc_info()[0]
  37. from openid.yadis import xri
  38. class XRDSError(Exception):
  39. """An error with the XRDS document."""
  40. # The exception that triggered this exception
  41. reason = None
  42. class XRDSFraud(XRDSError):
  43. """Raised when there's an assertion in the XRDS that it does not have
  44. the authority to make.
  45. """
  46. def parseXRDS(text):
  47. """Parse the given text as an XRDS document.
  48. @return: ElementTree containing an XRDS document
  49. @raises XRDSError: When there is a parse error or the document does
  50. not contain an XRDS.
  51. """
  52. try:
  53. element = ElementTree.XML(text)
  54. except XMLError, why:
  55. exc = XRDSError('Error parsing document as XML')
  56. exc.reason = why
  57. raise exc
  58. else:
  59. tree = ElementTree.ElementTree(element)
  60. if not isXRDS(tree):
  61. raise XRDSError('Not an XRDS document')
  62. return tree
  63. XRD_NS_2_0 = 'xri://$xrd*($v*2.0)'
  64. XRDS_NS = 'xri://$xrds'
  65. def nsTag(ns, t):
  66. return '{%s}%s' % (ns, t)
  67. def mkXRDTag(t):
  68. """basestring -> basestring
  69. Create a tag name in the XRD 2.0 XML namespace suitable for using
  70. with ElementTree
  71. """
  72. return nsTag(XRD_NS_2_0, t)
  73. def mkXRDSTag(t):
  74. """basestring -> basestring
  75. Create a tag name in the XRDS XML namespace suitable for using
  76. with ElementTree
  77. """
  78. return nsTag(XRDS_NS, t)
  79. # Tags that are used in Yadis documents
  80. root_tag = mkXRDSTag('XRDS')
  81. service_tag = mkXRDTag('Service')
  82. xrd_tag = mkXRDTag('XRD')
  83. type_tag = mkXRDTag('Type')
  84. uri_tag = mkXRDTag('URI')
  85. expires_tag = mkXRDTag('Expires')
  86. # Other XRD tags
  87. canonicalID_tag = mkXRDTag('CanonicalID')
  88. def isXRDS(xrd_tree):
  89. """Is this document an XRDS document?"""
  90. root = xrd_tree.getroot()
  91. return root.tag == root_tag
  92. def getYadisXRD(xrd_tree):
  93. """Return the XRD element that should contain the Yadis services"""
  94. xrd = None
  95. # for the side-effect of assigning the last one in the list to the
  96. # xrd variable
  97. for xrd in xrd_tree.findall(xrd_tag):
  98. pass
  99. # There were no elements found, or else xrd would be set to the
  100. # last one
  101. if xrd is None:
  102. raise XRDSError('No XRD present in tree')
  103. return xrd
  104. def getXRDExpiration(xrd_element, default=None):
  105. """Return the expiration date of this XRD element, or None if no
  106. expiration was specified.
  107. @type xrd_element: ElementTree node
  108. @param default: The value to use as the expiration if no
  109. expiration was specified in the XRD.
  110. @rtype: datetime.datetime
  111. @raises ValueError: If the xrd:Expires element is present, but its
  112. contents are not formatted according to the specification.
  113. """
  114. expires_element = xrd_element.find(expires_tag)
  115. if expires_element is None:
  116. return default
  117. else:
  118. expires_string = expires_element.text
  119. # Will raise ValueError if the string is not the expected format
  120. expires_time = strptime(expires_string, "%Y-%m-%dT%H:%M:%SZ")
  121. return datetime(*expires_time[0:6])
  122. def getCanonicalID(iname, xrd_tree):
  123. """Return the CanonicalID from this XRDS document.
  124. @param iname: the XRI being resolved.
  125. @type iname: unicode
  126. @param xrd_tree: The XRDS output from the resolver.
  127. @type xrd_tree: ElementTree
  128. @returns: The XRI CanonicalID or None.
  129. @returntype: unicode or None
  130. """
  131. xrd_list = xrd_tree.findall(xrd_tag)
  132. xrd_list.reverse()
  133. try:
  134. canonicalID = xri.XRI(xrd_list[0].findall(canonicalID_tag)[0].text)
  135. except IndexError:
  136. return None
  137. childID = canonicalID.lower()
  138. for xrd in xrd_list[1:]:
  139. # XXX: can't use rsplit until we require python >= 2.4.
  140. parent_sought = childID[:childID.rindex('!')]
  141. parent = xri.XRI(xrd.findtext(canonicalID_tag))
  142. if parent_sought != parent.lower():
  143. raise XRDSFraud("%r can not come from %s" % (childID, parent))
  144. childID = parent_sought
  145. root = xri.rootAuthority(iname)
  146. if not xri.providerIsAuthoritative(root, childID):
  147. raise XRDSFraud("%r can not come from root %r" % (childID, root))
  148. return canonicalID
  149. class _Max(object):
  150. """Value that compares greater than any other value.
  151. Should only be used as a singleton. Implemented for use as a
  152. priority value for when a priority is not specified."""
  153. def __cmp__(self, other):
  154. if other is self:
  155. return 0
  156. return 1
  157. Max = _Max()
  158. def getPriorityStrict(element):
  159. """Get the priority of this element.
  160. Raises ValueError if the value of the priority is invalid. If no
  161. priority is specified, it returns a value that compares greater
  162. than any other value.
  163. """
  164. prio_str = element.get('priority')
  165. if prio_str is not None:
  166. prio_val = int(prio_str)
  167. if prio_val >= 0:
  168. return prio_val
  169. else:
  170. raise ValueError('Priority values must be non-negative integers')
  171. # Any errors in parsing the priority fall through to here
  172. return Max
  173. def getPriority(element):
  174. """Get the priority of this element
  175. Returns Max if no priority is specified or the priority value is invalid.
  176. """
  177. try:
  178. return getPriorityStrict(element)
  179. except ValueError:
  180. return Max
  181. def prioSort(elements):
  182. """Sort a list of elements that have priority attributes"""
  183. # Randomize the services before sorting so that equal priority
  184. # elements are load-balanced.
  185. random.shuffle(elements)
  186. prio_elems = [(getPriority(e), e) for e in elements]
  187. prio_elems.sort()
  188. sorted_elems = [s for (_, s) in prio_elems]
  189. return sorted_elems
  190. def iterServices(xrd_tree):
  191. """Return an iterable over the Service elements in the Yadis XRD
  192. sorted by priority"""
  193. xrd = getYadisXRD(xrd_tree)
  194. return prioSort(xrd.findall(service_tag))
  195. def sortedURIs(service_element):
  196. """Given a Service element, return a list of the contents of all
  197. URI tags in priority order."""
  198. return [uri_element.text for uri_element
  199. in prioSort(service_element.findall(uri_tag))]
  200. def getTypeURIs(service_element):
  201. """Given a Service element, return a list of the contents of all
  202. Type tags"""
  203. return [type_element.text for type_element
  204. in service_element.findall(type_tag)]
  205. def expandService(service_element):
  206. """Take a service element and expand it into an iterator of:
  207. ([type_uri], uri, service_element)
  208. """
  209. uris = sortedURIs(service_element)
  210. if not uris:
  211. uris = [None]
  212. expanded = []
  213. for uri in uris:
  214. type_uris = getTypeURIs(service_element)
  215. expanded.append((type_uris, uri, service_element))
  216. return expanded
  217. def expandServices(service_elements):
  218. """Take a sorted iterator of service elements and expand it into a
  219. sorted iterator of:
  220. ([type_uri], uri, service_element)
  221. There may be more than one item in the resulting list for each
  222. service element if there is more than one URI or type for a
  223. service, but each triple will be unique.
  224. If there is no URI or Type for a Service element, it will not
  225. appear in the result.
  226. """
  227. expanded = []
  228. for service_element in service_elements:
  229. expanded.extend(expandService(service_element))
  230. return expanded