webfinger.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  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. from session import getJson
  16. from cache import storeWebfingerInCache
  17. from cache import getWebfingerFromCache
  18. def parseHandle(handle: str) -> (str,str):
  19. if '.' not in handle:
  20. return None, None
  21. if '/@' in handle:
  22. domain, nickname = \
  23. handle.replace('https://','').replace('http://','').replace('dat://','').split('/@')
  24. else:
  25. if '/users/' in handle:
  26. domain, nickname = \
  27. handle.replace('https://','').replace('http://','').replace('dat://','').split('/users/')
  28. else:
  29. if '@' in handle:
  30. nickname, domain = handle.split('@')
  31. else:
  32. return None, None
  33. return nickname, domain
  34. def webfingerHandle(session,handle: str,httpPrefix: str,cachedWebfingers: {}, \
  35. fromDomain: str,projectVersion: str) -> {}:
  36. if not session:
  37. print('WARN: No session specified for webfingerHandle')
  38. return None
  39. nickname, domain = parseHandle(handle)
  40. if not nickname:
  41. return None
  42. wfDomain=domain
  43. if ':' in wfDomain:
  44. #wfPort=int(wfDomain.split(':')[1])
  45. #if wfPort==80 or wfPort==443:
  46. wfDomain=wfDomain.split(':')[0]
  47. wf=getWebfingerFromCache(nickname+'@'+wfDomain,cachedWebfingers)
  48. if wf:
  49. return wf
  50. url = '{}://{}/.well-known/webfinger'.format(httpPrefix,domain)
  51. par = {'resource': 'acct:{}'.format(nickname+'@'+wfDomain)}
  52. hdr = {'Accept': 'application/jrd+json'}
  53. try:
  54. result = getJson(session, url, hdr, par,projectVersion,httpPrefix,fromDomain)
  55. except Exception as e:
  56. print("Unable to webfinger " + url)
  57. print('nickname: '+str(nickname))
  58. print('domain: '+str(wfDomain))
  59. print('headers: '+str(hdr))
  60. print('params: '+str(par))
  61. print(e)
  62. return None
  63. storeWebfingerInCache(nickname+'@'+wfDomain,result,cachedWebfingers)
  64. return result
  65. def generateMagicKey(publicKeyPem) -> str:
  66. """See magic_key method in
  67. https://github.com/tootsuite/mastodon/blob/707ddf7808f90e3ab042d7642d368c2ce8e95e6f/app/models/account.rb
  68. """
  69. privkey = RSA.importKey(publicKeyPem)
  70. mod = base64.urlsafe_b64encode(number.long_to_bytes(privkey.n)).decode("utf-8")
  71. pubexp = base64.urlsafe_b64encode(number.long_to_bytes(privkey.e)).decode("utf-8")
  72. return f"data:application/magic-public-key,RSA.{mod}.{pubexp}"
  73. def storeWebfingerEndpoint(nickname: str,domain: str,port: int,baseDir: str, \
  74. wfJson: {}) -> bool:
  75. """Stores webfinger endpoint for a user to a file
  76. """
  77. originalDomain=domain
  78. if port:
  79. if port!=80 and port!=443:
  80. if ':' not in domain:
  81. domain=domain+':'+str(port)
  82. handle=nickname+'@'+domain
  83. wfSubdir='/wfendpoints'
  84. if not os.path.isdir(baseDir+wfSubdir):
  85. os.mkdir(baseDir+wfSubdir)
  86. filename=baseDir+wfSubdir+'/'+handle.lower()+'.json'
  87. with open(filename, 'w') as fp:
  88. commentjson.dump(wfJson, fp, indent=4, sort_keys=False)
  89. if nickname=='inbox':
  90. handle=originalDomain+'@'+domain
  91. filename=baseDir+wfSubdir+'/'+handle.lower()+'.json'
  92. with open(filename, 'w') as fp:
  93. commentjson.dump(wfJson, fp, indent=4, sort_keys=False)
  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 webfingerMeta(httpPrefix: str,domainFull: str) -> str:
  143. """Return /.well-known/host-meta
  144. """
  145. #return \
  146. # "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" \
  147. # "<XRD xmlns=\"http://docs.oasis-open.org/ns/xri/xrd-1.0\">" \
  148. # "<Link rel=\"lrdd\" type=\"application/xrd+xml\" template=\""+httpPrefix+"://"+domainFull+"/.well-known/webfinger?resource={uri}\"/>" \
  149. # "</XRD>"
  150. return \
  151. "<?xml version=’1.0' encoding=’UTF-8'?>" \
  152. "<XRD xmlns=’http://docs.oasis-open.org/ns/xri/xrd-1.0'" \
  153. " xmlns:hm=’http://host-meta.net/xrd/1.0'>" \
  154. "" \
  155. "<hm:Host>"+domainFull+"</hm:Host>" \
  156. "" \
  157. "<Link rel=’lrdd’" \
  158. " template=’"+httpPrefix+"://"+domainFull+"/describe?uri={uri}'>" \
  159. " <Title>Resource Descriptor</Title>" \
  160. " </Link>" \
  161. "</XRD>"
  162. def webfingerLookup(path: str,baseDir: str,port: int,debug: bool) -> {}:
  163. """Lookup the webfinger endpoint for an account
  164. """
  165. if not path.startswith('/.well-known/webfinger?'):
  166. return None
  167. handle=None
  168. if 'resource=acct:' in path:
  169. handle=path.split('resource=acct:')[1].strip()
  170. if debug:
  171. print('DEBUG: WEBFINGER handle '+handle)
  172. else:
  173. if 'resource=acct%3A' in path:
  174. handle=path.split('resource=acct%3A')[1].replace('%40','@',1).replace('%3A',':',1).strip()
  175. if debug:
  176. print('DEBUG: WEBFINGER handle '+handle)
  177. if not handle:
  178. if debug:
  179. print('DEBUG: WEBFINGER handle missing')
  180. return None
  181. if '&' in handle:
  182. handle=handle.split('&')[0].strip()
  183. if debug:
  184. print('DEBUG: WEBFINGER handle with & removed '+handle)
  185. if '@' not in handle:
  186. if debug:
  187. print('DEBUG: WEBFINGER no @ in handle '+handle)
  188. return None
  189. if port:
  190. if port!=80 and port !=443:
  191. if ':' not in handle:
  192. handle=handle+':'+str(port)
  193. # convert @domain@domain to inbox@domain
  194. if '@' in handle:
  195. handleDomain=handle.split('@')[1]
  196. if handle.startswith(handleDomain+'@'):
  197. handle='inbox@'+handleDomain
  198. filename=baseDir+'/wfendpoints/'+handle.lower()+'.json'
  199. if debug:
  200. print('DEBUG: WEBFINGER filename '+filename)
  201. if not os.path.isfile(filename):
  202. if debug:
  203. print('DEBUG: WEBFINGER filename not found '+filename)
  204. return None
  205. wfJson={"nickname": "unknown"}
  206. with open(filename, 'r') as fp:
  207. wfJson=commentjson.load(fp)
  208. return wfJson