webfinger.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. __filename__ = "webfinger.py"
  2. __author__ = "Bob Mottram"
  3. __license__ = "AGPL3+"
  4. __version__ = "1.0.0"
  5. __maintainer__ = "Bob Mottram"
  6. __email__ = "bob@freedombone.net"
  7. __status__ = "Production"
  8. import base64
  9. from Crypto.PublicKey import RSA
  10. from Crypto.Util import number
  11. import requests
  12. import json
  13. import os
  14. import time
  15. from session import getJson
  16. from cache import storeWebfingerInCache
  17. from cache import getWebfingerFromCache
  18. from utils import loadJson
  19. from utils import saveJson
  20. def parseHandle(handle: str) -> (str,str):
  21. if '.' not in handle:
  22. return None, None
  23. if '/@' in handle:
  24. domain, nickname = \
  25. handle.replace('https://','').replace('http://','').replace('dat://','').split('/@')
  26. else:
  27. if '/users/' in handle:
  28. domain, nickname = \
  29. handle.replace('https://','').replace('http://','').replace('dat://','').split('/users/')
  30. else:
  31. if '@' in handle:
  32. nickname, domain = handle.split('@')
  33. else:
  34. return None, None
  35. return nickname, domain
  36. def webfingerHandle(session,handle: str,httpPrefix: str,cachedWebfingers: {}, \
  37. fromDomain: str,projectVersion: str) -> {}:
  38. if not session:
  39. print('WARN: No session specified for webfingerHandle')
  40. return None
  41. nickname, domain = parseHandle(handle)
  42. if not nickname:
  43. return None
  44. wfDomain=domain
  45. if ':' in wfDomain:
  46. #wfPort=int(wfDomain.split(':')[1])
  47. #if wfPort==80 or wfPort==443:
  48. wfDomain=wfDomain.split(':')[0]
  49. wf=getWebfingerFromCache(nickname+'@'+wfDomain,cachedWebfingers)
  50. if wf:
  51. return wf
  52. url = '{}://{}/.well-known/webfinger'.format(httpPrefix,domain)
  53. par = {'resource': 'acct:{}'.format(nickname+'@'+wfDomain)}
  54. hdr = {'Accept': 'application/jrd+json'}
  55. try:
  56. result = getJson(session, url, hdr, par,projectVersion,httpPrefix,fromDomain)
  57. except Exception as e:
  58. print("Unable to webfinger " + url)
  59. print('nickname: '+str(nickname))
  60. print('domain: '+str(wfDomain))
  61. print('headers: '+str(hdr))
  62. print('params: '+str(par))
  63. print(e)
  64. return None
  65. storeWebfingerInCache(nickname+'@'+wfDomain,result,cachedWebfingers)
  66. return result
  67. def generateMagicKey(publicKeyPem) -> str:
  68. """See magic_key method in
  69. https://github.com/tootsuite/mastodon/blob/707ddf7808f90e3ab042d7642d368c2ce8e95e6f/app/models/account.rb
  70. """
  71. privkey = RSA.importKey(publicKeyPem)
  72. mod = base64.urlsafe_b64encode(number.long_to_bytes(privkey.n)).decode("utf-8")
  73. pubexp = base64.urlsafe_b64encode(number.long_to_bytes(privkey.e)).decode("utf-8")
  74. return f"data:application/magic-public-key,RSA.{mod}.{pubexp}"
  75. def storeWebfingerEndpoint(nickname: str,domain: str,port: int,baseDir: str, \
  76. wfJson: {}) -> bool:
  77. """Stores webfinger endpoint for a user to a file
  78. """
  79. originalDomain=domain
  80. if port:
  81. if port!=80 and port!=443:
  82. if ':' not in domain:
  83. domain=domain+':'+str(port)
  84. handle=nickname+'@'+domain
  85. wfSubdir='/wfendpoints'
  86. if not os.path.isdir(baseDir+wfSubdir):
  87. os.mkdir(baseDir+wfSubdir)
  88. filename=baseDir+wfSubdir+'/'+handle.lower()+'.json'
  89. saveJson(wfJson,filename)
  90. if nickname=='inbox':
  91. handle=originalDomain+'@'+domain
  92. filename=baseDir+wfSubdir+'/'+handle.lower()+'.json'
  93. saveJson(wfJson,filename)
  94. return True
  95. def createWebfingerEndpoint(nickname: str,domain: str,port: int, \
  96. httpPrefix: str,publicKeyPem) -> {}:
  97. """Creates a webfinger endpoint for a user
  98. """
  99. originalDomain=domain
  100. if port:
  101. if port!=80 and port!=443:
  102. if ':' not in domain:
  103. domain=domain+':'+str(port)
  104. personName=nickname
  105. personId=httpPrefix+"://"+domain+"/users/"+personName
  106. subjectStr="acct:"+personName+"@"+originalDomain
  107. profilePageHref=httpPrefix+"://"+domain+"/@"+nickname
  108. if nickname=='inbox' or nickname==originalDomain:
  109. personName='actor'
  110. personId=httpPrefix+"://"+domain+"/"+personName
  111. subjectStr="acct:"+originalDomain+"@"+originalDomain
  112. profilePageHref=httpPrefix+'://'+domain+'/about/more?instance_actor=true'
  113. account = {
  114. "aliases": [
  115. httpPrefix+"://"+domain+"/@"+personName,
  116. personId
  117. ],
  118. "links": [
  119. {
  120. "href": profilePageHref,
  121. "rel": "http://webfinger.net/rel/profile-page",
  122. "type": "text/html"
  123. },
  124. {
  125. "href": httpPrefix+"://"+domain+"/users/"+nickname+".atom",
  126. "rel": "http://schemas.google.com/g/2010#updates-from",
  127. "type": "application/atom+xml"
  128. },
  129. {
  130. "href": personId,
  131. "rel": "self",
  132. "type": "application/activity+json"
  133. },
  134. {
  135. "href": generateMagicKey(publicKeyPem),
  136. "rel": "magic-public-key"
  137. }
  138. ],
  139. "subject": subjectStr
  140. }
  141. return account
  142. def webfingerNodeInfo(httpPrefix: str,domainFull: str) -> {}:
  143. """ /.well-known/nodeinfo endpoint
  144. """
  145. nodeinfo = {
  146. 'links': [
  147. {
  148. 'href': httpPrefix+'://'+domainFull+'/nodeinfo/2.0',
  149. 'rel': 'http://nodeinfo.diaspora.software/ns/schema/2.0'
  150. }
  151. ]
  152. }
  153. return nodeinfo
  154. def webfingerMeta(httpPrefix: str,domainFull: str) -> str:
  155. """Return /.well-known/host-meta
  156. """
  157. metaStr="<?xml version=’1.0' encoding=’UTF-8'?>"
  158. metaStr+="<XRD xmlns=’http://docs.oasis-open.org/ns/xri/xrd-1.0'"
  159. metaStr+=" xmlns:hm=’http://host-meta.net/xrd/1.0'>"
  160. metaStr+=""
  161. metaStr+="<hm:Host>"+domainFull+"</hm:Host>"
  162. metaStr+=""
  163. metaStr+="<Link rel=’lrdd’"
  164. metaStr+=" template=’"+httpPrefix+"://"+domainFull+"/describe?uri={uri}'>"
  165. metaStr+=" <Title>Resource Descriptor</Title>"
  166. metaStr+=" </Link>"
  167. metaStr+="</XRD>"
  168. return metaStr
  169. def webfingerLookup(path: str,baseDir: str,port: int,debug: bool) -> {}:
  170. """Lookup the webfinger endpoint for an account
  171. """
  172. if not path.startswith('/.well-known/webfinger?'):
  173. return None
  174. handle=None
  175. if 'resource=acct:' in path:
  176. handle=path.split('resource=acct:')[1].strip()
  177. if debug:
  178. print('DEBUG: WEBFINGER handle '+handle)
  179. else:
  180. if 'resource=acct%3A' in path:
  181. handle=path.split('resource=acct%3A')[1].replace('%40','@',1).replace('%3A',':',1).strip()
  182. if debug:
  183. print('DEBUG: WEBFINGER handle '+handle)
  184. if not handle:
  185. if debug:
  186. print('DEBUG: WEBFINGER handle missing')
  187. return None
  188. if '&' in handle:
  189. handle=handle.split('&')[0].strip()
  190. if debug:
  191. print('DEBUG: WEBFINGER handle with & removed '+handle)
  192. if '@' not in handle:
  193. if debug:
  194. print('DEBUG: WEBFINGER no @ in handle '+handle)
  195. return None
  196. if port:
  197. if port!=80 and port !=443:
  198. if ':' not in handle:
  199. handle=handle+':'+str(port)
  200. # convert @domain@domain to inbox@domain
  201. if '@' in handle:
  202. handleDomain=handle.split('@')[1]
  203. if handle.startswith(handleDomain+'@'):
  204. handle='inbox@'+handleDomain
  205. filename=baseDir+'/wfendpoints/'+handle.lower()+'.json'
  206. if debug:
  207. print('DEBUG: WEBFINGER filename '+filename)
  208. if not os.path.isfile(filename):
  209. if debug:
  210. print('DEBUG: WEBFINGER filename not found '+filename)
  211. return None
  212. wfJson=loadJson(filename)
  213. if not wfJson:
  214. wfJson={"nickname": "unknown"}
  215. return wfJson