2019-06-28 18:55:29 +00:00
|
|
|
|
__filename__ = "webfinger.py"
|
|
|
|
|
__author__ = "Bob Mottram"
|
|
|
|
|
__license__ = "AGPL3+"
|
|
|
|
|
__version__ = "0.0.1"
|
|
|
|
|
__maintainer__ = "Bob Mottram"
|
|
|
|
|
__email__ = "bob@freedombone.net"
|
|
|
|
|
__status__ = "Production"
|
|
|
|
|
|
|
|
|
|
import base64
|
|
|
|
|
from Crypto.PublicKey import RSA
|
|
|
|
|
from Crypto.Util import number
|
|
|
|
|
import requests
|
|
|
|
|
import json
|
|
|
|
|
import commentjson
|
|
|
|
|
import os
|
|
|
|
|
from session import getJson
|
2019-06-30 15:03:26 +00:00
|
|
|
|
from cache import storeWebfingerInCache
|
|
|
|
|
from cache import getWebfingerFromCache
|
2019-06-28 18:55:29 +00:00
|
|
|
|
|
2019-07-01 11:09:09 +00:00
|
|
|
|
def parseHandle(handle: str) -> (str,str):
|
2019-06-28 18:55:29 +00:00
|
|
|
|
if '.' not in handle:
|
|
|
|
|
return None, None
|
|
|
|
|
if '/@' in handle:
|
2019-07-03 09:40:27 +00:00
|
|
|
|
domain, nickname = \
|
2019-07-03 19:00:03 +00:00
|
|
|
|
handle.replace('https://','').replace('http://','').replace('dat://','').split('/@')
|
2019-06-28 18:55:29 +00:00
|
|
|
|
else:
|
|
|
|
|
if '/users/' in handle:
|
2019-07-03 09:40:27 +00:00
|
|
|
|
domain, nickname = \
|
2019-07-03 19:00:03 +00:00
|
|
|
|
handle.replace('https://','').replace('http://','').replace('dat://','').split('/users/')
|
2019-06-28 18:55:29 +00:00
|
|
|
|
else:
|
|
|
|
|
if '@' in handle:
|
2019-07-03 09:40:27 +00:00
|
|
|
|
nickname, domain = handle.split('@')
|
2019-06-28 18:55:29 +00:00
|
|
|
|
else:
|
|
|
|
|
return None, None
|
|
|
|
|
|
2019-07-03 09:40:27 +00:00
|
|
|
|
return nickname, domain
|
2019-06-28 18:55:29 +00:00
|
|
|
|
|
|
|
|
|
|
2019-07-03 19:00:03 +00:00
|
|
|
|
def webfingerHandle(session,handle: str,httpPrefix: str,cachedWebfingers: {}) -> {}:
|
2019-07-03 09:40:27 +00:00
|
|
|
|
nickname, domain = parseHandle(handle)
|
|
|
|
|
if not nickname:
|
2019-06-28 18:55:29 +00:00
|
|
|
|
return None
|
2019-07-01 21:01:43 +00:00
|
|
|
|
wfDomain=domain
|
|
|
|
|
if ':' in wfDomain:
|
|
|
|
|
wfDomain=wfDomain.split(':')[0]
|
2019-07-03 09:40:27 +00:00
|
|
|
|
wf=getWebfingerFromCache(nickname+'@'+wfDomain,cachedWebfingers)
|
2019-06-30 15:03:26 +00:00
|
|
|
|
if wf:
|
|
|
|
|
return wf
|
2019-07-03 19:00:03 +00:00
|
|
|
|
url = '{}://{}/.well-known/webfinger'.format(httpPrefix,domain)
|
2019-07-03 09:40:27 +00:00
|
|
|
|
par = {'resource': 'acct:{}'.format(nickname+'@'+wfDomain)}
|
2019-06-28 18:55:29 +00:00
|
|
|
|
hdr = {'Accept': 'application/jrd+json'}
|
2019-07-04 17:31:41 +00:00
|
|
|
|
try:
|
|
|
|
|
result = getJson(session, url, hdr, par)
|
|
|
|
|
except:
|
|
|
|
|
return None
|
2019-07-01 21:01:43 +00:00
|
|
|
|
# print("Unable to webfinger " + url + ' ' + str(hdr) + ' ' + str(par))
|
2019-07-03 09:40:27 +00:00
|
|
|
|
storeWebfingerInCache(nickname+'@'+wfDomain,result,cachedWebfingers)
|
2019-06-28 18:55:29 +00:00
|
|
|
|
return result
|
|
|
|
|
|
2019-07-01 11:09:09 +00:00
|
|
|
|
def generateMagicKey(publicKeyPem) -> str:
|
2019-06-28 18:55:29 +00:00
|
|
|
|
"""See magic_key method in
|
|
|
|
|
https://github.com/tootsuite/mastodon/blob/707ddf7808f90e3ab042d7642d368c2ce8e95e6f/app/models/account.rb
|
|
|
|
|
"""
|
|
|
|
|
privkey = RSA.importKey(publicKeyPem)
|
|
|
|
|
mod = base64.urlsafe_b64encode(number.long_to_bytes(privkey.n)).decode("utf-8")
|
|
|
|
|
pubexp = base64.urlsafe_b64encode(number.long_to_bytes(privkey.e)).decode("utf-8")
|
|
|
|
|
return f"data:application/magic-public-key,RSA.{mod}.{pubexp}"
|
|
|
|
|
|
2019-07-03 09:40:27 +00:00
|
|
|
|
def storeWebfingerEndpoint(nickname: str,domain: str,baseDir: str, \
|
2019-07-02 20:54:22 +00:00
|
|
|
|
wfJson: {}) -> bool:
|
2019-06-28 18:55:29 +00:00
|
|
|
|
"""Stores webfinger endpoint for a user to a file
|
|
|
|
|
"""
|
2019-07-03 09:40:27 +00:00
|
|
|
|
handle=nickname+'@'+domain
|
2019-06-28 18:55:29 +00:00
|
|
|
|
wfSubdir='/wfendpoints'
|
|
|
|
|
if not os.path.isdir(baseDir+wfSubdir):
|
|
|
|
|
os.mkdir(baseDir+wfSubdir)
|
|
|
|
|
filename=baseDir+wfSubdir+'/'+handle.lower()+'.json'
|
|
|
|
|
with open(filename, 'w') as fp:
|
|
|
|
|
commentjson.dump(wfJson, fp, indent=4, sort_keys=False)
|
|
|
|
|
return True
|
|
|
|
|
|
2019-07-03 09:40:27 +00:00
|
|
|
|
def createWebfingerEndpoint(nickname: str,domain: str,port: int, \
|
2019-07-03 19:00:03 +00:00
|
|
|
|
httpPrefix: str,publicKeyPem) -> {}:
|
2019-06-28 18:55:29 +00:00
|
|
|
|
"""Creates a webfinger endpoint for a user
|
|
|
|
|
"""
|
2019-06-30 18:23:18 +00:00
|
|
|
|
if port!=80 and port!=443:
|
|
|
|
|
domain=domain+':'+str(port)
|
|
|
|
|
|
2019-06-28 18:55:29 +00:00
|
|
|
|
account = {
|
|
|
|
|
"aliases": [
|
2019-07-03 19:00:03 +00:00
|
|
|
|
httpPrefix+"://"+domain+"/@"+nickname,
|
|
|
|
|
httpPrefix+"://"+domain+"/users/"+nickname
|
2019-06-28 18:55:29 +00:00
|
|
|
|
],
|
|
|
|
|
"links": [
|
|
|
|
|
{
|
2019-07-03 19:00:03 +00:00
|
|
|
|
"href": httpPrefix+"://"+domain+"/@"+nickname,
|
2019-06-28 18:55:29 +00:00
|
|
|
|
"rel": "http://webfinger.net/rel/profile-page",
|
|
|
|
|
"type": "text/html"
|
|
|
|
|
},
|
|
|
|
|
{
|
2019-07-03 19:00:03 +00:00
|
|
|
|
"href": httpPrefix+"://"+domain+"/users/"+nickname+".atom",
|
2019-06-28 18:55:29 +00:00
|
|
|
|
"rel": "http://schemas.google.com/g/2010#updates-from",
|
|
|
|
|
"type": "application/atom+xml"
|
|
|
|
|
},
|
|
|
|
|
{
|
2019-07-03 19:00:03 +00:00
|
|
|
|
"href": httpPrefix+"://"+domain+"/users/"+nickname,
|
2019-06-28 18:55:29 +00:00
|
|
|
|
"rel": "self",
|
|
|
|
|
"type": "application/activity+json"
|
|
|
|
|
},
|
|
|
|
|
{
|
2019-07-03 19:00:03 +00:00
|
|
|
|
"href": httpPrefix+"://"+domain+"/api/salmon/1",
|
2019-06-28 18:55:29 +00:00
|
|
|
|
"rel": "salmon"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"href": generateMagicKey(publicKeyPem),
|
|
|
|
|
"rel": "magic-public-key"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"rel": "http://ostatus.org/schema/1.0/subscribe",
|
2019-07-03 19:00:03 +00:00
|
|
|
|
"template": httpPrefix+"://"+domain+"/authorize_interaction?uri={uri}"
|
2019-06-28 18:55:29 +00:00
|
|
|
|
}
|
|
|
|
|
],
|
2019-07-03 09:40:27 +00:00
|
|
|
|
"subject": "acct:"+nickname+"@"+domain
|
2019-06-28 18:55:29 +00:00
|
|
|
|
}
|
|
|
|
|
return account
|
|
|
|
|
|
|
|
|
|
def webfingerMeta() -> str:
|
|
|
|
|
"""
|
|
|
|
|
"""
|
|
|
|
|
return "<?xml version=’1.0' encoding=’UTF-8'?>" \
|
|
|
|
|
"<XRD xmlns=’http://docs.oasis-open.org/ns/xri/xrd-1.0'" \
|
|
|
|
|
" xmlns:hm=’http://host-meta.net/xrd/1.0'>" \
|
|
|
|
|
"" \
|
|
|
|
|
"<hm:Host>example.com</hm:Host>" \
|
|
|
|
|
"" \
|
|
|
|
|
"<Link rel=’lrdd’" \
|
|
|
|
|
" template=’http://example.com/describe?uri={uri}'>" \
|
|
|
|
|
" <Title>Resource Descriptor</Title>" \
|
|
|
|
|
" </Link>" \
|
|
|
|
|
"</XRD>"
|
|
|
|
|
|
2019-07-01 11:09:09 +00:00
|
|
|
|
def webfingerLookup(path: str,baseDir: str) -> {}:
|
2019-06-28 18:55:29 +00:00
|
|
|
|
"""Lookup the webfinger endpoint for an account
|
|
|
|
|
"""
|
|
|
|
|
if not path.startswith('/.well-known/webfinger?'):
|
|
|
|
|
return None
|
|
|
|
|
handle=None
|
|
|
|
|
if 'resource=acct:' in path:
|
|
|
|
|
handle=path.split('resource=acct:')[1].strip()
|
|
|
|
|
else:
|
|
|
|
|
if 'resource=acct%3A' in path:
|
2019-07-01 21:01:43 +00:00
|
|
|
|
handle=path.split('resource=acct%3A')[1].replace('%40','@').strip()
|
2019-06-28 18:55:29 +00:00
|
|
|
|
if not handle:
|
|
|
|
|
return None
|
|
|
|
|
if '&' in handle:
|
|
|
|
|
handle=handle.split('&')[0].strip()
|
|
|
|
|
if '@' not in handle:
|
|
|
|
|
return None
|
|
|
|
|
filename=baseDir+'/wfendpoints/'+handle.lower()+'.json'
|
|
|
|
|
if not os.path.isfile(filename):
|
|
|
|
|
return None
|
2019-07-03 09:42:54 +00:00
|
|
|
|
wfJson={"nickname": "unknown"}
|
2019-06-28 18:55:29 +00:00
|
|
|
|
with open(filename, 'r') as fp:
|
|
|
|
|
wfJson=commentjson.load(fp)
|
|
|
|
|
return wfJson
|