webfinger.py 7.7 KB

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