epicyon/webfinger.py

247 lines
8.4 KiB
Python
Raw Normal View History

2019-06-28 18:55:29 +00:00
__filename__ = "webfinger.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
2019-08-29 13:35:29 +00:00
__version__ = "1.0.0"
2019-06-28 18:55:29 +00:00
__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
2019-10-11 18:03:58 +00:00
import time
2019-06-28 18:55:29 +00:00
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-08-14 20:12:27 +00:00
def webfingerHandle(session,handle: str,httpPrefix: str,cachedWebfingers: {}, \
fromDomain: str,projectVersion: str) -> {}:
2019-07-16 10:19:04 +00:00
if not session:
print('WARN: No session specified for webfingerHandle')
return None
2019-07-19 13:32:58 +00:00
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:
#wfPort=int(wfDomain.split(':')[1])
#if wfPort==80 or wfPort==443:
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)}
hdr = {'Accept': 'application/jrd+json'}
2019-07-04 17:31:41 +00:00
try:
2019-08-14 20:12:27 +00:00
result = getJson(session, url, hdr, par,projectVersion,httpPrefix,fromDomain)
except Exception as e:
2019-07-19 13:32:58 +00:00
print("Unable to webfinger " + url)
print('nickname: '+str(nickname))
print('domain: '+str(wfDomain))
2019-07-19 13:32:58 +00:00
print('headers: '+str(hdr))
print('params: '+str(par))
print(e)
2019-07-04 17:31:41 +00:00
return None
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-19 14:03:34 +00:00
def storeWebfingerEndpoint(nickname: str,domain: str,port: int,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-08-23 20:03:06 +00:00
originalDomain=domain
if port:
if port!=80 and port!=443:
if ':' not in domain:
domain=domain+':'+str(port)
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'
2019-10-12 09:37:21 +00:00
tries=0
while tries<5:
2019-09-30 22:39:02 +00:00
try:
with open(filename, 'w') as fp:
2019-10-18 18:57:34 +00:00
commentjson.dump(wfJson, fp, indent=2, sort_keys=False)
2019-10-12 09:37:21 +00:00
break
2019-09-30 22:39:02 +00:00
except Exception as e:
print(e)
2019-10-12 09:37:21 +00:00
time.sleep(1)
tries+=1
if nickname=='inbox':
handle=originalDomain+'@'+domain
filename=baseDir+wfSubdir+'/'+handle.lower()+'.json'
tries=0
while tries<5:
try:
with open(filename, 'w') as fp:
2019-10-18 18:57:34 +00:00
commentjson.dump(wfJson, fp, indent=2, sort_keys=False)
2019-10-12 09:37:21 +00:00
break
except Exception as e:
print(e)
time.sleep(1)
tries+=1
2019-06-28 18:55:29 +00:00
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
"""
originalDomain=domain
if port:
if port!=80 and port!=443:
if ':' not in domain:
domain=domain+':'+str(port)
2019-06-30 18:23:18 +00:00
2019-08-23 20:03:06 +00:00
personName=nickname
personId=httpPrefix+"://"+domain+"/users/"+personName
subjectStr="acct:"+personName+"@"+originalDomain
2019-08-26 15:20:14 +00:00
profilePageHref=httpPrefix+"://"+domain+"/@"+nickname
2019-08-23 20:03:06 +00:00
if nickname=='inbox' or nickname==originalDomain:
personName='actor'
personId=httpPrefix+"://"+domain+"/"+personName
subjectStr="acct:"+originalDomain+"@"+originalDomain
2019-08-26 15:20:14 +00:00
profilePageHref=httpPrefix+'://'+domain+'/about/more?instance_actor=true'
2019-06-28 18:55:29 +00:00
account = {
"aliases": [
2019-08-23 20:03:06 +00:00
httpPrefix+"://"+domain+"/@"+personName,
personId
2019-06-28 18:55:29 +00:00
],
"links": [
{
2019-08-26 15:20:14 +00:00
"href": profilePageHref,
2019-06-28 18:55:29 +00:00
"rel": "http://webfinger.net/rel/profile-page",
"type": "text/html"
},
2019-08-26 14:30:09 +00:00
{
"href": httpPrefix+"://"+domain+"/users/"+nickname+".atom",
"rel": "http://schemas.google.com/g/2010#updates-from",
"type": "application/atom+xml"
},
2019-06-28 18:55:29 +00:00
{
2019-08-23 20:03:06 +00:00
"href": personId,
2019-06-28 18:55:29 +00:00
"rel": "self",
"type": "application/activity+json"
},
{
"href": generateMagicKey(publicKeyPem),
"rel": "magic-public-key"
}
],
2019-08-23 20:05:16 +00:00
"subject": subjectStr
2019-06-28 18:55:29 +00:00
}
return account
2019-08-16 20:52:55 +00:00
def webfingerMeta(httpPrefix: str,domainFull: str) -> str:
"""Return /.well-known/host-meta
2019-06-28 18:55:29 +00:00
"""
2019-08-26 14:30:09 +00:00
#return \
# "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" \
# "<XRD xmlns=\"http://docs.oasis-open.org/ns/xri/xrd-1.0\">" \
# "<Link rel=\"lrdd\" type=\"application/xrd+xml\" template=\""+httpPrefix+"://"+domainFull+"/.well-known/webfinger?resource={uri}\"/>" \
# "</XRD>"
2019-08-16 20:52:55 +00:00
return \
2019-08-26 14:30:09 +00:00
"<?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>"+domainFull+"</hm:Host>" \
"" \
"<Link rel=lrdd" \
" template="+httpPrefix+"://"+domainFull+"/describe?uri={uri}'>" \
" <Title>Resource Descriptor</Title>" \
" </Link>" \
2019-08-16 20:52:55 +00:00
"</XRD>"
def webfingerLookup(path: str,baseDir: str,port: int,debug: bool) -> {}:
2019-06-28 18:55:29 +00:00
"""Lookup the webfinger endpoint for an account
"""
2019-07-19 14:19:36 +00:00
if not path.startswith('/.well-known/webfinger?'):
2019-06-28 18:55:29 +00:00
return None
handle=None
if 'resource=acct:' in path:
2019-07-19 14:19:36 +00:00
handle=path.split('resource=acct:')[1].strip()
if debug:
print('DEBUG: WEBFINGER handle '+handle)
2019-06-28 18:55:29 +00:00
else:
if 'resource=acct%3A' in path:
handle=path.split('resource=acct%3A')[1].replace('%40','@',1).replace('%3A',':',1).strip()
2019-07-19 14:19:36 +00:00
if debug:
print('DEBUG: WEBFINGER handle '+handle)
2019-06-28 18:55:29 +00:00
if not handle:
2019-07-19 14:19:36 +00:00
if debug:
print('DEBUG: WEBFINGER handle missing')
2019-06-28 18:55:29 +00:00
return None
if '&' in handle:
handle=handle.split('&')[0].strip()
2019-07-19 14:19:36 +00:00
if debug:
print('DEBUG: WEBFINGER handle with & removed '+handle)
2019-06-28 18:55:29 +00:00
if '@' not in handle:
2019-07-19 14:19:36 +00:00
if debug:
print('DEBUG: WEBFINGER no @ in handle '+handle)
2019-06-28 18:55:29 +00:00
return None
if port:
if port!=80 and port !=443:
if ':' not in handle:
handle=handle+':'+str(port)
2019-08-23 14:18:31 +00:00
# convert @domain@domain to inbox@domain
if '@' in handle:
handleDomain=handle.split('@')[1]
2019-08-23 14:19:33 +00:00
if handle.startswith(handleDomain+'@'):
2019-08-23 14:18:31 +00:00
handle='inbox@'+handleDomain
2019-06-28 18:55:29 +00:00
filename=baseDir+'/wfendpoints/'+handle.lower()+'.json'
2019-07-19 14:19:36 +00:00
if debug:
print('DEBUG: WEBFINGER filename '+filename)
2019-06-28 18:55:29 +00:00
if not os.path.isfile(filename):
2019-07-19 14:19:36 +00:00
if debug:
print('DEBUG: WEBFINGER filename not found '+filename)
2019-06-28 18:55:29 +00:00
return None
2019-07-03 09:42:54 +00:00
wfJson={"nickname": "unknown"}
2019-10-11 18:03:58 +00:00
tries=0
while tries<5:
try:
with open(filename, 'r') as fp:
wfJson=commentjson.load(fp)
break
except Exception as e:
2019-10-17 10:26:56 +00:00
print('WARN: commentjson exception webfingerLookup - '+str(e))
2019-10-11 18:03:58 +00:00
time.sleep(1)
tries+=1
2019-06-28 18:55:29 +00:00
return wfJson