webfinger.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. __filename__ = "webfinger.py"
  2. __author__ = "Bob Mottram"
  3. __license__ = "AGPL3+"
  4. __version__ = "0.0.1"
  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('headers: '+str(hdr))
  58. print('params: '+str(par))
  59. print(e)
  60. return None
  61. storeWebfingerInCache(nickname+'@'+wfDomain,result,cachedWebfingers)
  62. return result
  63. def generateMagicKey(publicKeyPem) -> str:
  64. """See magic_key method in
  65. https://github.com/tootsuite/mastodon/blob/707ddf7808f90e3ab042d7642d368c2ce8e95e6f/app/models/account.rb
  66. """
  67. privkey = RSA.importKey(publicKeyPem)
  68. mod = base64.urlsafe_b64encode(number.long_to_bytes(privkey.n)).decode("utf-8")
  69. pubexp = base64.urlsafe_b64encode(number.long_to_bytes(privkey.e)).decode("utf-8")
  70. return f"data:application/magic-public-key,RSA.{mod}.{pubexp}"
  71. def storeWebfingerEndpoint(nickname: str,domain: str,port: int,baseDir: str, \
  72. wfJson: {}) -> bool:
  73. """Stores webfinger endpoint for a user to a file
  74. """
  75. if port:
  76. if port!=80 and port!=443:
  77. if ':' not in domain:
  78. domain=domain+':'+str(port)
  79. handle=nickname+'@'+domain
  80. wfSubdir='/wfendpoints'
  81. if not os.path.isdir(baseDir+wfSubdir):
  82. os.mkdir(baseDir+wfSubdir)
  83. filename=baseDir+wfSubdir+'/'+handle.lower()+'.json'
  84. with open(filename, 'w') as fp:
  85. commentjson.dump(wfJson, fp, indent=4, sort_keys=False)
  86. return True
  87. def createWebfingerEndpoint(nickname: str,domain: str,port: int, \
  88. httpPrefix: str,publicKeyPem) -> {}:
  89. """Creates a webfinger endpoint for a user
  90. """
  91. originalDomain=domain
  92. if port:
  93. if port!=80 and port!=443:
  94. if ':' not in domain:
  95. domain=domain+':'+str(port)
  96. account = {
  97. "aliases": [
  98. httpPrefix+"://"+domain+"/@"+nickname,
  99. httpPrefix+"://"+domain+"/users/"+nickname
  100. ],
  101. "links": [
  102. {
  103. "href": httpPrefix+"://"+domain+"/@"+nickname,
  104. "rel": "http://webfinger.net/rel/profile-page",
  105. "type": "text/html"
  106. },
  107. {
  108. "href": httpPrefix+"://"+domain+"/users/"+nickname+".atom",
  109. "rel": "http://schemas.google.com/g/2010#updates-from",
  110. "type": "application/atom+xml"
  111. },
  112. {
  113. "href": httpPrefix+"://"+domain+"/users/"+nickname,
  114. "rel": "self",
  115. "type": "application/activity+json"
  116. },
  117. {
  118. "href": httpPrefix+"://"+domain+"/api/salmon/1",
  119. "rel": "salmon"
  120. },
  121. {
  122. "href": generateMagicKey(publicKeyPem),
  123. "rel": "magic-public-key"
  124. }
  125. ],
  126. "subject": "acct:"+nickname+"@"+originalDomain
  127. }
  128. return account
  129. def webfingerMeta(httpPrefix: str,domainFull: str) -> str:
  130. """Return /.well-known/host-meta
  131. """
  132. return \
  133. "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" \
  134. "<XRD xmlns=\"http://docs.oasis-open.org/ns/xri/xrd-1.0\">" \
  135. "<Link rel=\"lrdd\" type=\"application/xrd+xml\" template=\""+httpPrefix+"://"+domainFull+"/.well-known/webfinger?resource={uri}\"/>" \
  136. "</XRD>"
  137. def webfingerLookup(path: str,baseDir: str,port: int,debug: bool) -> {}:
  138. """Lookup the webfinger endpoint for an account
  139. """
  140. if not path.startswith('/.well-known/webfinger?'):
  141. return None
  142. handle=None
  143. if 'resource=acct:' in path:
  144. handle=path.split('resource=acct:')[1].strip()
  145. if debug:
  146. print('DEBUG: WEBFINGER handle '+handle)
  147. else:
  148. if 'resource=acct%3A' in path:
  149. handle=path.split('resource=acct%3A')[1].replace('%40','@',1).replace('%3A',':',1).strip()
  150. if debug:
  151. print('DEBUG: WEBFINGER handle '+handle)
  152. if not handle:
  153. if debug:
  154. print('DEBUG: WEBFINGER handle missing')
  155. return None
  156. if '&' in handle:
  157. handle=handle.split('&')[0].strip()
  158. if debug:
  159. print('DEBUG: WEBFINGER handle with & removed '+handle)
  160. if '@' not in handle:
  161. if debug:
  162. print('DEBUG: WEBFINGER no @ in handle '+handle)
  163. return None
  164. if port:
  165. if port!=80 and port !=443:
  166. if ':' not in handle:
  167. handle=handle+':'+str(port)
  168. filename=baseDir+'/wfendpoints/'+handle.lower()+'.json'
  169. if debug:
  170. print('DEBUG: WEBFINGER filename '+filename)
  171. if not os.path.isfile(filename):
  172. if debug:
  173. print('DEBUG: WEBFINGER filename not found '+filename)
  174. return None
  175. wfJson={"nickname": "unknown"}
  176. with open(filename, 'r') as fp:
  177. wfJson=commentjson.load(fp)
  178. return wfJson