core.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. import datetime
  2. import logging
  3. RFC6415_TYPE = 'application/xrd+xml'
  4. RFC7033_TYPE = 'application/jrd+json'
  5. JRD_TYPES = ('application/jrd+json', 'application/xrd+json', 'application/json', 'text/json')
  6. XRD_TYPES = ('application/xrd+xml', 'text/xml')
  7. KNOWN_RELS = {
  8. 'activity_streams': 'http://activitystrea.ms/spec/1.0',
  9. 'app': ('http://apinamespace.org/atom', 'application/atomsvc+xml'),
  10. 'avatar': 'http://webfinger.net/rel/avatar',
  11. 'foaf': ('describedby', 'application/rdf+xml'),
  12. 'hcard': 'http://microformats.org/profile/hcard',
  13. 'oauth_access_token': 'http://apinamespace.org/oauth/access_token',
  14. 'oauth_authorize': 'http://apinamespace.org/oauth/authorize',
  15. 'oauth_request_token': 'http://apinamespace.org/oauth/request_token',
  16. 'openid': 'http://specs.openid.net/auth/2.0/provider',
  17. 'opensocial': 'http://ns.opensocial.org/2008/opensocial/activitystreams',
  18. 'portable_contacts': 'http://portablecontacts.net/spec/1.0',
  19. 'profile': 'http://webfinger.net/rel/profile-page',
  20. 'updates_from': 'http://schemas.google.com/g/2010#updates-from',
  21. 'ostatus_sub': 'http://ostatus.org/schema/1.0/subscribe',
  22. 'salmon_endpoint': 'salmon',
  23. 'salmon_key': 'magic-public-key',
  24. 'webfist': 'http://webfist.org/spec/rel',
  25. 'xfn': 'http://gmpg.org/xfn/11',
  26. 'jrd': ('lrdd', 'application/json'),
  27. 'webfinger': ('lrdd', 'application/jrd+json'),
  28. 'xrd': ('lrdd', 'application/xrd+xml'),
  29. }
  30. logger = logging.getLogger("rd")
  31. def _is_str(s):
  32. try:
  33. return isinstance(s, str)
  34. except NameError:
  35. return isinstance(s, str)
  36. def loads(content, content_type):
  37. from rd import jrd, xrd
  38. content_type = content_type.split(";")[0]
  39. if content_type in JRD_TYPES:
  40. logger.debug("loads() loading JRD")
  41. return jrd.loads(content)
  42. elif content_type in XRD_TYPES:
  43. logger.debug("loads() loading XRD")
  44. return xrd.loads(content)
  45. raise TypeError('Unknown content type')
  46. #
  47. # helper functions for host parsing and discovery
  48. #
  49. def parse_uri_components(resource, default_scheme='https'):
  50. hostname = None
  51. scheme = default_scheme
  52. from urllib.parse import urlparse
  53. parts = urlparse(resource)
  54. if parts.scheme and parts.netloc:
  55. scheme = parts.scheme
  56. ''' FIXME: if we have https://user@some.example/ we end up with parts.netloc='user@some.example' here. '''
  57. hostname = parts.netloc
  58. path = parts.path
  59. elif parts.scheme == 'acct' or (not parts.scheme and '@' in parts.path):
  60. ''' acct: means we expect WebFinger to work, and RFC7033 requires https, so host-meta should support it too. '''
  61. scheme = 'https'
  62. ''' We should just have user@site.example here, but if it instead
  63. is user@site.example/whatever/else we have to split it later
  64. on the first slash character, '/'.
  65. '''
  66. hostname = parts.path.split('@')[-1]
  67. path = None
  68. ''' In case we have hostname=='site.example/whatever/else' we do the split
  69. on the first slash, giving us 'site.example' and 'whatever/else'.
  70. '''
  71. if '/' in hostname:
  72. (hostname, path) = hostname.split('/', maxsplit=1)
  73. ''' Normalize path so it always starts with /, which is the behaviour of urlparse too. '''
  74. path = '/' + path
  75. else:
  76. if not parts.path:
  77. raise ValueError('No hostname could be deduced from arguments.')
  78. elif '/' in parts.path:
  79. (hostname, path) = parts.path.split('/', maxsplit=1)
  80. ''' Normalize path so it always starts with /, which is the behaviour of urlparse too. '''
  81. path = '/' + path
  82. else:
  83. hostname = parts.path
  84. path = None
  85. return (scheme, hostname, path)
  86. #
  87. # special XRD types
  88. #
  89. class Attribute(object):
  90. def __init__(self, name, value):
  91. self.name = name
  92. self.value = value
  93. def __cmp__(self, other):
  94. return cmp(str(self), str(other))
  95. def __eq__(self, other):
  96. return str(self) == other
  97. def __str__(self):
  98. return "%s=%s" % (self.name, self.value)
  99. class Element(object):
  100. def __init__(self, name, value, attrs=None):
  101. self.name = name
  102. self.value = value
  103. self.attrs = attrs or {}
  104. class Title(object):
  105. def __init__(self, value, lang=None):
  106. self.value = value
  107. self.lang = lang
  108. def __cmp__(self, other):
  109. return cmp(str(self), str(other))
  110. def __eq__(self, other):
  111. return str(self) == str(other)
  112. def __str__(self):
  113. if self.lang:
  114. return "%s:%s" % (self.lang, self.value)
  115. return self.value
  116. class Property(object):
  117. def __init__(self, type_, value=None):
  118. self.type = type_
  119. self.value = value
  120. def __cmp__(self, other):
  121. return cmp(str(self), str(other))
  122. def __eq__(self, other):
  123. return str(self) == other
  124. def __str__(self):
  125. if self.value:
  126. return "%s:%s" % (self.type, self.value)
  127. return self.type
  128. #
  129. # special list types
  130. #
  131. class ListLikeObject(list):
  132. def __setitem__(self, key, value):
  133. value = self.item(value)
  134. super(ListLikeObject, self).__setitem__(key, value)
  135. def append(self, value):
  136. value = self.item(value)
  137. super(ListLikeObject, self).append(value)
  138. def extend(self, values):
  139. values = (self.item(value) for value in values)
  140. super(ListLikeObject, self).extend(values)
  141. class AttributeList(ListLikeObject):
  142. def __call__(self, name):
  143. for attr in self:
  144. if attr.name == name:
  145. yield attr
  146. def item(self, value):
  147. if isinstance(value, (list, tuple)):
  148. return Attribute(*value)
  149. elif not isinstance(value, Attribute):
  150. raise ValueError('value must be an instance of Attribute')
  151. return value
  152. class ElementList(ListLikeObject):
  153. def item(self, value):
  154. if not isinstance(value, Element):
  155. raise ValueError('value must be an instance of Type')
  156. return value
  157. class TitleList(ListLikeObject):
  158. def item(self, value):
  159. if _is_str(value):
  160. return Title(value)
  161. elif isinstance(value, (list, tuple)):
  162. return Title(*value)
  163. elif not isinstance(value, Title):
  164. raise ValueError('value must be an instance of Title')
  165. return value
  166. class LinkList(ListLikeObject):
  167. def __call__(self, rel):
  168. for link in self:
  169. if link.rel == rel:
  170. yield link
  171. def item(self, value):
  172. if not isinstance(value, Link):
  173. raise ValueError('value must be an instance of Link')
  174. return value
  175. class PropertyList(ListLikeObject):
  176. def __call__(self, type_):
  177. for prop in self:
  178. if prop.type == type_:
  179. yield prop
  180. def item(self, value):
  181. if _is_str(value):
  182. return Property(value)
  183. elif isinstance(value, (tuple, list)):
  184. return Property(*value)
  185. elif not isinstance(value, Property):
  186. raise ValueError('value must be an instance of Property')
  187. return value
  188. #
  189. # Link object
  190. #
  191. class Link(object):
  192. def __init__(self, rel=None, type=None, href=None, template=None):
  193. self.rel = rel
  194. self.type = type
  195. self.href = href
  196. self.template = template
  197. self._titles = TitleList()
  198. self._properties = PropertyList()
  199. def get_titles(self):
  200. return self._titles
  201. titles = property(get_titles)
  202. def get_properties(self):
  203. return self._properties
  204. properties = property(get_properties)
  205. def apply_template(self, uri):
  206. from urllib.parse import quote
  207. if not self.template:
  208. raise TypeError('This is not a template Link')
  209. return self.template.replace('{uri}', quote(uri, safe=''))
  210. def __str__(self):
  211. from cgi import escape
  212. attrs = ''
  213. for prop in ['rel', 'type', 'href', 'template']:
  214. val = getattr(self, prop)
  215. if val:
  216. attrs += ' {!s}="{!s}"'.format(escape(prop), escape(val))
  217. return '<Link{!s}/>'.format(attrs)
  218. #
  219. # main RD class
  220. #
  221. class RD(object):
  222. def __init__(self, xml_id=None, subject=None):
  223. self.xml_id = xml_id
  224. self.subject = subject
  225. self._expires = None
  226. self._aliases = []
  227. self._properties = PropertyList()
  228. self._links = LinkList()
  229. self._signatures = []
  230. self._attributes = AttributeList()
  231. self._elements = ElementList()
  232. # ser/deser methods
  233. def to_json(self):
  234. from rd import jrd
  235. return jrd.dumps(self)
  236. def to_xml(self):
  237. from rd import xrd
  238. return xrd.dumps(self)
  239. # helper methods
  240. def find_link(self, rels, attr=None, mimetype=None):
  241. if not isinstance(rels, (list, tuple)):
  242. rels = (rels,)
  243. for link in self.links:
  244. if link.rel in rels:
  245. if mimetype and link.type != mimetype:
  246. continue
  247. if attr:
  248. return getattr(link, attr, None)
  249. return link
  250. def __getattr__(self, name, attr=None):
  251. if name in KNOWN_RELS:
  252. try:
  253. ''' If we have a specific mimetype for this rel value '''
  254. rel, mimetype = KNOWN_RELS[name]
  255. except ValueError:
  256. rel = KNOWN_RELS[name]
  257. mimetype = None
  258. return self.find_link(rel, attr=attr, mimetype=mimetype)
  259. raise AttributeError(name)
  260. # custom elements and attributes
  261. def get_elements(self):
  262. return self._elements
  263. elements = property(get_elements)
  264. @property
  265. def attributes(self):
  266. return self._attributes
  267. # defined elements and attributes
  268. def get_expires(self):
  269. return self._expires
  270. def set_expires(self, expires):
  271. if not isinstance(expires, datetime.datetime):
  272. raise ValueError('expires must be a datetime object')
  273. self._expires = expires
  274. expires = property(get_expires, set_expires)
  275. def get_aliases(self):
  276. return self._aliases
  277. aliases = property(get_aliases)
  278. def get_properties(self):
  279. return self._properties
  280. properties = property(get_properties)
  281. def get_links(self):
  282. return self._links
  283. links = property(get_links)
  284. def get_signatures(self):
  285. return self._signatures
  286. signatures = property(get_links)