message.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  1. """Extension argument processing code
  2. """
  3. __all__ = [
  4. 'Message', 'NamespaceMap', 'no_default', 'registerNamespaceAlias',
  5. 'OPENID_NS', 'BARE_NS', 'OPENID1_NS', 'OPENID2_NS', 'SREG_URI',
  6. 'IDENTIFIER_SELECT'
  7. ]
  8. import copy
  9. import warnings
  10. import urllib.request
  11. import urllib.error
  12. from openid import oidutil
  13. from openid import kvform
  14. try:
  15. ElementTree = oidutil.importElementTree()
  16. except ImportError:
  17. # No elementtree found, so give up, but don't fail to import,
  18. # since we have fallbacks.
  19. ElementTree = None
  20. # This doesn't REALLY belong here, but where is better?
  21. IDENTIFIER_SELECT = 'http://specs.openid.net/auth/2.0/identifier_select'
  22. # URI for Simple Registration extension, the only commonly deployed
  23. # OpenID 1.x extension, and so a special case
  24. SREG_URI = 'http://openid.net/sreg/1.0'
  25. # The OpenID 1.X namespace URI
  26. OPENID1_NS = 'http://openid.net/signon/1.0'
  27. THE_OTHER_OPENID1_NS = 'http://openid.net/signon/1.1'
  28. OPENID1_NAMESPACES = OPENID1_NS, THE_OTHER_OPENID1_NS
  29. # The OpenID 2.0 namespace URI
  30. OPENID2_NS = 'http://specs.openid.net/auth/2.0'
  31. # The namespace consisting of pairs with keys that are prefixed with
  32. # "openid." but not in another namespace.
  33. NULL_NAMESPACE = oidutil.Symbol('Null namespace')
  34. # The null namespace, when it is an allowed OpenID namespace
  35. OPENID_NS = oidutil.Symbol('OpenID namespace')
  36. # The top-level namespace, excluding all pairs with keys that start
  37. # with "openid."
  38. BARE_NS = oidutil.Symbol('Bare namespace')
  39. # Limit, in bytes, of identity provider and return_to URLs, including
  40. # response payload. See OpenID 1.1 specification, Appendix D.
  41. OPENID1_URL_LIMIT = 2047
  42. # All OpenID protocol fields. Used to check namespace aliases.
  43. OPENID_PROTOCOL_FIELDS = [
  44. 'ns',
  45. 'mode',
  46. 'error',
  47. 'return_to',
  48. 'contact',
  49. 'reference',
  50. 'signed',
  51. 'assoc_type',
  52. 'session_type',
  53. 'dh_modulus',
  54. 'dh_gen',
  55. 'dh_consumer_public',
  56. 'claimed_id',
  57. 'identity',
  58. 'realm',
  59. 'invalidate_handle',
  60. 'op_endpoint',
  61. 'response_nonce',
  62. 'sig',
  63. 'assoc_handle',
  64. 'trust_root',
  65. 'openid',
  66. ]
  67. class UndefinedOpenIDNamespace(ValueError):
  68. """Raised if the generic OpenID namespace is accessed when there
  69. is no OpenID namespace set for this message."""
  70. class InvalidOpenIDNamespace(ValueError):
  71. """Raised if openid.ns is not a recognized value.
  72. For recognized values, see L{Message.allowed_openid_namespaces}
  73. """
  74. def __str__(self):
  75. s = "Invalid OpenID Namespace"
  76. if self.args:
  77. s += " %r" % (self.args[0], )
  78. return s
  79. # Sentinel used for Message implementation to indicate that getArg
  80. # should raise an exception instead of returning a default.
  81. no_default = object()
  82. # Global namespace / alias registration map. See
  83. # registerNamespaceAlias.
  84. registered_aliases = {}
  85. class NamespaceAliasRegistrationError(Exception):
  86. """
  87. Raised when an alias or namespace URI has already been registered.
  88. """
  89. pass
  90. def registerNamespaceAlias(namespace_uri, alias):
  91. """
  92. Registers a (namespace URI, alias) mapping in a global namespace
  93. alias map. Raises NamespaceAliasRegistrationError if either the
  94. namespace URI or alias has already been registered with a
  95. different value. This function is required if you want to use a
  96. namespace with an OpenID 1 message.
  97. """
  98. global registered_aliases
  99. if registered_aliases.get(alias) == namespace_uri:
  100. return
  101. if namespace_uri in list(registered_aliases.values()):
  102. raise NamespaceAliasRegistrationError(
  103. 'Namespace uri %r already registered' % (namespace_uri, ))
  104. if alias in registered_aliases:
  105. raise NamespaceAliasRegistrationError('Alias %r already registered' %
  106. (alias, ))
  107. registered_aliases[alias] = namespace_uri
  108. class Message(object):
  109. """
  110. In the implementation of this object, None represents the global
  111. namespace as well as a namespace with no key.
  112. @cvar namespaces: A dictionary specifying specific
  113. namespace-URI to alias mappings that should be used when
  114. generating namespace aliases.
  115. @ivar ns_args: two-level dictionary of the values in this message,
  116. grouped by namespace URI. The first level is the namespace
  117. URI.
  118. """
  119. allowed_openid_namespaces = [OPENID1_NS, THE_OTHER_OPENID1_NS, OPENID2_NS]
  120. def __init__(self, openid_namespace=None):
  121. """Create an empty Message.
  122. @raises InvalidOpenIDNamespace: if openid_namespace is not in
  123. L{Message.allowed_openid_namespaces}
  124. """
  125. self.args = {}
  126. self.namespaces = NamespaceMap()
  127. if openid_namespace is None:
  128. self._openid_ns_uri = None
  129. else:
  130. implicit = openid_namespace in OPENID1_NAMESPACES
  131. self.setOpenIDNamespace(openid_namespace, implicit)
  132. @classmethod
  133. def fromPostArgs(cls, args):
  134. """Construct a Message containing a set of POST arguments.
  135. """
  136. self = cls()
  137. # Partition into "openid." args and bare args
  138. openid_args = {}
  139. for key, value in args.items():
  140. if isinstance(value, list):
  141. raise TypeError("query dict must have one value for each key, "
  142. "not lists of values. Query is %r" % (args, ))
  143. try:
  144. prefix, rest = key.split('.', 1)
  145. except ValueError:
  146. prefix = None
  147. if prefix != 'openid':
  148. self.args[(BARE_NS, key)] = value
  149. else:
  150. openid_args[rest] = value
  151. self._fromOpenIDArgs(openid_args)
  152. return self
  153. @classmethod
  154. def fromOpenIDArgs(cls, openid_args):
  155. """Construct a Message from a parsed KVForm message.
  156. @raises InvalidOpenIDNamespace: if openid.ns is not in
  157. L{Message.allowed_openid_namespaces}
  158. """
  159. self = cls()
  160. self._fromOpenIDArgs(openid_args)
  161. return self
  162. def _fromOpenIDArgs(self, openid_args):
  163. ns_args = []
  164. # Resolve namespaces
  165. for rest, value in openid_args.items():
  166. try:
  167. ns_alias, ns_key = rest.split('.', 1)
  168. except ValueError:
  169. ns_alias = NULL_NAMESPACE
  170. ns_key = rest
  171. if ns_alias == 'ns':
  172. self.namespaces.addAlias(value, ns_key)
  173. elif ns_alias == NULL_NAMESPACE and ns_key == 'ns':
  174. # null namespace
  175. self.setOpenIDNamespace(value, False)
  176. else:
  177. ns_args.append((ns_alias, ns_key, value))
  178. # Implicitly set an OpenID namespace definition (OpenID 1)
  179. if not self.getOpenIDNamespace():
  180. self.setOpenIDNamespace(OPENID1_NS, True)
  181. # Actually put the pairs into the appropriate namespaces
  182. for (ns_alias, ns_key, value) in ns_args:
  183. ns_uri = self.namespaces.getNamespaceURI(ns_alias)
  184. if ns_uri is None:
  185. # we found a namespaced arg without a namespace URI defined
  186. ns_uri = self._getDefaultNamespace(ns_alias)
  187. if ns_uri is None:
  188. ns_uri = self.getOpenIDNamespace()
  189. ns_key = '%s.%s' % (ns_alias, ns_key)
  190. else:
  191. self.namespaces.addAlias(ns_uri, ns_alias, implicit=True)
  192. self.setArg(ns_uri, ns_key, value)
  193. def _getDefaultNamespace(self, mystery_alias):
  194. """OpenID 1 compatibility: look for a default namespace URI to
  195. use for this alias."""
  196. global registered_aliases
  197. # Only try to map an alias to a default if it's an
  198. # OpenID 1.x message.
  199. if self.isOpenID1():
  200. return registered_aliases.get(mystery_alias)
  201. else:
  202. return None
  203. def setOpenIDNamespace(self, openid_ns_uri, implicit):
  204. """Set the OpenID namespace URI used in this message.
  205. @raises InvalidOpenIDNamespace: if the namespace is not in
  206. L{Message.allowed_openid_namespaces}
  207. """
  208. if isinstance(openid_ns_uri, bytes):
  209. openid_ns_uri = str(openid_ns_uri, encoding="utf-8")
  210. if openid_ns_uri not in self.allowed_openid_namespaces:
  211. raise InvalidOpenIDNamespace(openid_ns_uri)
  212. self.namespaces.addAlias(openid_ns_uri, NULL_NAMESPACE, implicit)
  213. self._openid_ns_uri = openid_ns_uri
  214. def getOpenIDNamespace(self):
  215. return self._openid_ns_uri
  216. def isOpenID1(self):
  217. return self.getOpenIDNamespace() in OPENID1_NAMESPACES
  218. def isOpenID2(self):
  219. return self.getOpenIDNamespace() == OPENID2_NS
  220. def fromKVForm(cls, kvform_string):
  221. """Create a Message from a KVForm string"""
  222. return cls.fromOpenIDArgs(kvform.kvToDict(kvform_string))
  223. fromKVForm = classmethod(fromKVForm)
  224. def copy(self):
  225. return copy.deepcopy(self)
  226. def toPostArgs(self):
  227. """
  228. Return all arguments with openid. in front of namespaced arguments.
  229. @return bytes
  230. """
  231. args = {}
  232. # Add namespace definitions to the output
  233. for ns_uri, alias in self.namespaces.items():
  234. if self.namespaces.isImplicit(ns_uri):
  235. continue
  236. if alias == NULL_NAMESPACE:
  237. ns_key = 'openid.ns'
  238. else:
  239. ns_key = 'openid.ns.' + alias
  240. args[ns_key] = oidutil.toUnicode(ns_uri)
  241. for (ns_uri, ns_key), value in self.args.items():
  242. key = self.getKey(ns_uri, ns_key)
  243. # Ensure the resulting value is an UTF-8 encoded *bytestring*.
  244. args[key] = oidutil.toUnicode(value)
  245. return args
  246. def toArgs(self):
  247. """Return all namespaced arguments, failing if any
  248. non-namespaced arguments exist."""
  249. # FIXME - undocumented exception
  250. post_args = self.toPostArgs()
  251. kvargs = {}
  252. for k, v in post_args.items():
  253. if not k.startswith('openid.'):
  254. raise ValueError(
  255. 'This message can only be encoded as a POST, because it '
  256. 'contains arguments that are not prefixed with "openid."')
  257. else:
  258. kvargs[k[7:]] = v
  259. return kvargs
  260. def toFormMarkup(self,
  261. action_url,
  262. form_tag_attrs=None,
  263. submit_text="Continue"):
  264. """Generate HTML form markup that contains the values in this
  265. message, to be HTTP POSTed as x-www-form-urlencoded UTF-8.
  266. @param action_url: The URL to which the form will be POSTed
  267. @type action_url: str
  268. @param form_tag_attrs: Dictionary of attributes to be added to
  269. the form tag. 'accept-charset' and 'enctype' have defaults
  270. that can be overridden. If a value is supplied for
  271. 'action' or 'method', it will be replaced.
  272. @type form_tag_attrs: {unicode: unicode}
  273. @param submit_text: The text that will appear on the submit
  274. button for this form.
  275. @type submit_text: unicode
  276. @returns: A string containing (X)HTML markup for a form that
  277. encodes the values in this Message object.
  278. @rtype: str
  279. """
  280. if ElementTree is None:
  281. raise RuntimeError('This function requires ElementTree.')
  282. assert action_url is not None
  283. form = ElementTree.Element('form')
  284. if form_tag_attrs:
  285. for name, attr in form_tag_attrs.items():
  286. form.attrib[name] = attr
  287. form.attrib['action'] = oidutil.toUnicode(action_url)
  288. form.attrib['method'] = 'post'
  289. form.attrib['accept-charset'] = 'UTF-8'
  290. form.attrib['enctype'] = 'application/x-www-form-urlencoded'
  291. for name, value in self.toPostArgs().items():
  292. attrs = {
  293. 'type': 'hidden',
  294. 'name': oidutil.toUnicode(name),
  295. 'value': oidutil.toUnicode(value)
  296. }
  297. form.append(ElementTree.Element('input', attrs))
  298. submit = ElementTree.Element(
  299. 'input',
  300. {'type': 'submit',
  301. 'value': oidutil.toUnicode(submit_text)})
  302. form.append(submit)
  303. return str(ElementTree.tostring(form, encoding='utf-8'),
  304. encoding="utf-8")
  305. def toURL(self, base_url):
  306. """Generate a GET URL with the parameters in this message
  307. attached as query parameters."""
  308. return oidutil.appendArgs(base_url, self.toPostArgs())
  309. def toKVForm(self):
  310. """Generate a KVForm string that contains the parameters in
  311. this message. This will fail if the message contains arguments
  312. outside of the 'openid.' prefix.
  313. """
  314. return kvform.dictToKV(self.toArgs())
  315. def toURLEncoded(self):
  316. """Generate an x-www-urlencoded string"""
  317. args = sorted(self.toPostArgs().items())
  318. return urllib.parse.urlencode(args)
  319. def _fixNS(self, namespace):
  320. """Convert an input value into the internally used values of
  321. this object
  322. @param namespace: The string or constant to convert
  323. @type namespace: str or unicode or BARE_NS or OPENID_NS
  324. """
  325. if isinstance(namespace, bytes):
  326. namespace = str(namespace, encoding="utf-8")
  327. if namespace == OPENID_NS:
  328. if self._openid_ns_uri is None:
  329. raise UndefinedOpenIDNamespace('OpenID namespace not set')
  330. else:
  331. namespace = self._openid_ns_uri
  332. if namespace != BARE_NS and not isinstance(namespace, str):
  333. raise TypeError(
  334. "Namespace must be BARE_NS, OPENID_NS or a string. got %r" %
  335. (namespace, ))
  336. if namespace != BARE_NS and ':' not in namespace:
  337. fmt = 'OpenID 2.0 namespace identifiers SHOULD be URIs. Got %r'
  338. warnings.warn(fmt % (namespace, ), DeprecationWarning)
  339. if namespace == 'sreg':
  340. fmt = 'Using %r instead of "sreg" as namespace'
  341. warnings.warn(
  342. fmt % (SREG_URI, ),
  343. DeprecationWarning, )
  344. return SREG_URI
  345. return namespace
  346. def hasKey(self, namespace, ns_key):
  347. namespace = self._fixNS(namespace)
  348. return (namespace, ns_key) in self.args
  349. def getKey(self, namespace, ns_key):
  350. """Get the key for a particular namespaced argument"""
  351. namespace = self._fixNS(namespace)
  352. if namespace == BARE_NS:
  353. return ns_key
  354. ns_alias = self.namespaces.getAlias(namespace)
  355. # No alias is defined, so no key can exist
  356. if ns_alias is None:
  357. return None
  358. if ns_alias == NULL_NAMESPACE:
  359. tail = ns_key
  360. else:
  361. tail = '%s.%s' % (ns_alias, ns_key)
  362. return 'openid.' + tail
  363. def getArg(self, namespace, key, default=None):
  364. """Get a value for a namespaced key.
  365. @param namespace: The namespace in the message for this key
  366. @type namespace: str
  367. @param key: The key to get within this namespace
  368. @type key: str
  369. @param default: The value to use if this key is absent from
  370. this message. Using the special value
  371. openid.message.no_default will result in this method
  372. raising a KeyError instead of returning the default.
  373. @rtype: str or the type of default
  374. @raises KeyError: if default is no_default
  375. @raises UndefinedOpenIDNamespace: if the message has not yet
  376. had an OpenID namespace set
  377. """
  378. namespace = self._fixNS(namespace)
  379. args_key = (namespace, key)
  380. try:
  381. return self.args[args_key]
  382. except KeyError:
  383. if default is no_default:
  384. raise KeyError((namespace, key))
  385. else:
  386. return default
  387. def getArgs(self, namespace):
  388. """Get the arguments that are defined for this namespace URI
  389. @returns: mapping from namespaced keys to values
  390. @returntype: dict of {str:bytes}
  391. """
  392. namespace = self._fixNS(namespace)
  393. args = []
  394. for ((pair_ns, ns_key), value) in self.args.items():
  395. if pair_ns == namespace:
  396. if isinstance(ns_key, bytes):
  397. k = str(ns_key, encoding="utf-8")
  398. else:
  399. k = ns_key
  400. if isinstance(value, bytes):
  401. v = str(value, encoding="utf-8")
  402. else:
  403. v = value
  404. args.append((k, v))
  405. return dict(args)
  406. def updateArgs(self, namespace, updates):
  407. """Set multiple key/value pairs in one call
  408. @param updates: The values to set
  409. @type updates: {unicode:unicode}
  410. """
  411. namespace = self._fixNS(namespace)
  412. for k, v in updates.items():
  413. self.setArg(namespace, k, v)
  414. def setArg(self, namespace, key, value):
  415. """Set a single argument in this namespace"""
  416. assert key is not None
  417. assert value is not None
  418. namespace = self._fixNS(namespace)
  419. # try to ensure that internally it's consistent, at least: str -> str
  420. if isinstance(value, bytes):
  421. value = str(value, encoding="utf-8")
  422. self.args[(namespace, key)] = value
  423. if not (namespace is BARE_NS):
  424. self.namespaces.add(namespace)
  425. def delArg(self, namespace, key):
  426. namespace = self._fixNS(namespace)
  427. del self.args[(namespace, key)]
  428. def __repr__(self):
  429. return "<%s.%s %r>" % (self.__class__.__module__,
  430. self.__class__.__name__, self.args)
  431. def __eq__(self, other):
  432. return self.args == other.args
  433. def __ne__(self, other):
  434. return not (self == other)
  435. def getAliasedArg(self, aliased_key, default=None):
  436. if aliased_key == 'ns':
  437. return self.getOpenIDNamespace()
  438. if aliased_key.startswith('ns.'):
  439. uri = self.namespaces.getNamespaceURI(aliased_key[3:])
  440. if uri is None:
  441. if default == no_default:
  442. raise KeyError
  443. else:
  444. return default
  445. else:
  446. return uri
  447. try:
  448. alias, key = aliased_key.split('.', 1)
  449. except ValueError:
  450. # need more than x values to unpack
  451. ns = None
  452. else:
  453. ns = self.namespaces.getNamespaceURI(alias)
  454. if ns is None:
  455. key = aliased_key
  456. ns = self.getOpenIDNamespace()
  457. return self.getArg(ns, key, default)
  458. class NamespaceMap(object):
  459. """Maintains a bijective map between namespace uris and aliases.
  460. """
  461. def __init__(self):
  462. self.alias_to_namespace = {}
  463. self.namespace_to_alias = {}
  464. self.implicit_namespaces = []
  465. def getAlias(self, namespace_uri):
  466. return self.namespace_to_alias.get(namespace_uri)
  467. def getNamespaceURI(self, alias):
  468. return self.alias_to_namespace.get(alias)
  469. def iterNamespaceURIs(self):
  470. """Return an iterator over the namespace URIs"""
  471. return iter(self.namespace_to_alias)
  472. def iterAliases(self):
  473. """Return an iterator over the aliases"""
  474. return iter(self.alias_to_namespace)
  475. def items(self):
  476. """Iterate over the mapping
  477. @returns: iterator of (namespace_uri, alias)
  478. """
  479. return self.namespace_to_alias.items()
  480. def addAlias(self, namespace_uri, desired_alias, implicit=False):
  481. """Add an alias from this namespace URI to the desired alias
  482. """
  483. if isinstance(namespace_uri, bytes):
  484. namespace_uri = str(namespace_uri, encoding="utf-8")
  485. # Check that desired_alias is not an openid protocol field as
  486. # per the spec.
  487. assert desired_alias not in OPENID_PROTOCOL_FIELDS, \
  488. "%r is not an allowed namespace alias" % (desired_alias,)
  489. # Check that desired_alias does not contain a period as per
  490. # the spec.
  491. if isinstance(desired_alias, str):
  492. assert '.' not in desired_alias, \
  493. "%r must not contain a dot" % (desired_alias,)
  494. # Check that there is not a namespace already defined for
  495. # the desired alias
  496. current_namespace_uri = self.alias_to_namespace.get(desired_alias)
  497. if (current_namespace_uri is not None and
  498. current_namespace_uri != namespace_uri):
  499. fmt = ('Cannot map %r to alias %r. '
  500. '%r is already mapped to alias %r')
  501. msg = fmt % (namespace_uri, desired_alias, current_namespace_uri,
  502. desired_alias)
  503. raise KeyError(msg)
  504. # Check that there is not already a (different) alias for
  505. # this namespace URI
  506. alias = self.namespace_to_alias.get(namespace_uri)
  507. if alias is not None and alias != desired_alias:
  508. fmt = ('Cannot map %r to alias %r. '
  509. 'It is already mapped to alias %r')
  510. raise KeyError(fmt % (namespace_uri, desired_alias, alias))
  511. assert (desired_alias == NULL_NAMESPACE or
  512. type(desired_alias) in [str, str]), repr(desired_alias)
  513. assert namespace_uri not in self.implicit_namespaces
  514. self.alias_to_namespace[desired_alias] = namespace_uri
  515. self.namespace_to_alias[namespace_uri] = desired_alias
  516. if implicit:
  517. self.implicit_namespaces.append(namespace_uri)
  518. return desired_alias
  519. def add(self, namespace_uri):
  520. """Add this namespace URI to the mapping, without caring what
  521. alias it ends up with"""
  522. # See if this namespace is already mapped to an alias
  523. alias = self.namespace_to_alias.get(namespace_uri)
  524. if alias is not None:
  525. return alias
  526. # Fall back to generating a numerical alias
  527. i = 0
  528. while True:
  529. alias = 'ext' + str(i)
  530. try:
  531. self.addAlias(namespace_uri, alias)
  532. except KeyError:
  533. i += 1
  534. else:
  535. return alias
  536. assert False, "Not reached"
  537. def isDefined(self, namespace_uri):
  538. return namespace_uri in self.namespace_to_alias
  539. def __contains__(self, namespace_uri):
  540. return self.isDefined(namespace_uri)
  541. def isImplicit(self, namespace_uri):
  542. return namespace_uri in self.implicit_namespaces