2020-04-04 14:14:25 +00:00
|
|
|
|
__filename__ = "webfinger.py"
|
|
|
|
|
__author__ = "Bob Mottram"
|
|
|
|
|
__license__ = "AGPL3+"
|
|
|
|
|
__version__ = "1.1.0"
|
|
|
|
|
__maintainer__ = "Bob Mottram"
|
|
|
|
|
__email__ = "bob@freedombone.net"
|
|
|
|
|
__status__ = "Production"
|
2019-06-28 18:55:29 +00:00
|
|
|
|
|
|
|
|
|
import base64
|
2020-03-04 09:59:08 +00:00
|
|
|
|
try:
|
|
|
|
|
from Cryptodome.PublicKey import RSA
|
|
|
|
|
from Cryptodome.Util import number
|
|
|
|
|
except ImportError:
|
|
|
|
|
from Crypto.PublicKey import RSA
|
|
|
|
|
from Crypto.Util import number
|
2019-06-28 18:55:29 +00:00
|
|
|
|
import os
|
|
|
|
|
from session import getJson
|
2019-06-30 15:03:26 +00:00
|
|
|
|
from cache import storeWebfingerInCache
|
|
|
|
|
from cache import getWebfingerFromCache
|
2019-10-22 11:55:06 +00:00
|
|
|
|
from utils import loadJson
|
2020-03-02 14:35:44 +00:00
|
|
|
|
from utils import loadJsonOnionify
|
2019-10-22 11:55:06 +00:00
|
|
|
|
from utils import saveJson
|
2019-06-28 18:55:29 +00:00
|
|
|
|
|
2020-04-04 14:14:25 +00:00
|
|
|
|
|
|
|
|
|
def parseHandle(handle: str) -> (str, str):
|
2019-06-28 18:55:29 +00:00
|
|
|
|
if '.' not in handle:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
return None, None
|
|
|
|
|
handleStr = handle.replace('https://', '').replace('http://', '')
|
|
|
|
|
handleStr = handleStr.replace('dat://', '').replace('i2p://', '')
|
2019-06-28 18:55:29 +00:00
|
|
|
|
if '/@' in handle:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
domain, nickname = handleStr.split('/@')
|
2019-06-28 18:55:29 +00:00
|
|
|
|
else:
|
|
|
|
|
if '/users/' in handle:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
domain, nickname = handleStr.split('/users/')
|
2019-06-28 18:55:29 +00:00
|
|
|
|
else:
|
|
|
|
|
if '@' in handle:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
nickname, domain = handle.split('@')
|
2019-06-28 18:55:29 +00:00
|
|
|
|
else:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
return None, None
|
|
|
|
|
return nickname, domain
|
2019-06-28 18:55:29 +00:00
|
|
|
|
|
|
|
|
|
|
2020-04-04 14:14:25 +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
|
|
|
|
|
2020-04-04 14:14:25 +00:00
|
|
|
|
nickname, domain = parseHandle(handle)
|
2019-07-03 09:40:27 +00:00
|
|
|
|
if not nickname:
|
2019-06-28 18:55:29 +00:00
|
|
|
|
return None
|
2020-04-04 14:14:25 +00:00
|
|
|
|
wfDomain = domain
|
2019-07-01 21:01:43 +00:00
|
|
|
|
if ':' in wfDomain:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
# wfPortStr=wfDomain.split(':')[1]
|
|
|
|
|
# if wfPortStr.isdigit():
|
|
|
|
|
# wfPort=int(wfPortStr)
|
|
|
|
|
# if wfPort==80 or wfPort==443:
|
|
|
|
|
wfDomain = wfDomain.split(':')[0]
|
|
|
|
|
wf = getWebfingerFromCache(nickname + '@' + wfDomain,
|
|
|
|
|
cachedWebfingers)
|
2019-06-30 15:03:26 +00:00
|
|
|
|
if wf:
|
2020-03-22 21:16:02 +00:00
|
|
|
|
return wf
|
2020-04-04 14:14:25 +00:00
|
|
|
|
url = '{}://{}/.well-known/webfinger'.format(httpPrefix, domain)
|
|
|
|
|
par = {
|
|
|
|
|
'resource': 'acct:{}'.format(nickname + '@' + wfDomain)
|
2020-03-22 20:36:19 +00:00
|
|
|
|
}
|
2020-04-04 14:14:25 +00:00
|
|
|
|
hdr = {
|
2020-03-22 20:36:19 +00:00
|
|
|
|
'Accept': 'application/jrd+json'
|
|
|
|
|
}
|
2019-07-04 17:31:41 +00:00
|
|
|
|
try:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
result = \
|
|
|
|
|
getJson(session, url, hdr, par, projectVersion,
|
|
|
|
|
httpPrefix, fromDomain)
|
2019-08-14 22:24:51 +00:00
|
|
|
|
except Exception as e:
|
2019-07-19 13:32:58 +00:00
|
|
|
|
print("Unable to webfinger " + url)
|
2020-04-04 14:14:25 +00:00
|
|
|
|
print('nickname: ' + str(nickname))
|
|
|
|
|
print('domain: ' + str(wfDomain))
|
|
|
|
|
print('headers: ' + str(hdr))
|
|
|
|
|
print('params: ' + str(par))
|
2019-08-14 22:24:51 +00:00
|
|
|
|
print(e)
|
2019-07-04 17:31:41 +00:00
|
|
|
|
return None
|
2020-04-04 14:14:25 +00:00
|
|
|
|
storeWebfingerInCache(nickname + '@' + wfDomain,
|
|
|
|
|
result, cachedWebfingers)
|
2019-06-28 18:55:29 +00:00
|
|
|
|
return result
|
|
|
|
|
|
2020-04-04 14:14:25 +00:00
|
|
|
|
|
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
|
2020-04-04 14:14:25 +00:00
|
|
|
|
https://github.com/tootsuite/mastodon/blob/
|
|
|
|
|
707ddf7808f90e3ab042d7642d368c2ce8e95e6f/app/models/account.rb
|
2019-06-28 18:55:29 +00:00
|
|
|
|
"""
|
2020-04-04 14:14:25 +00:00
|
|
|
|
privkey = RSA.importKey(publicKeyPem)
|
|
|
|
|
modBytes = number.long_to_bytes(privkey.n)
|
|
|
|
|
mod = base64.urlsafe_b64encode(modBytes).decode("utf-8")
|
|
|
|
|
expBytes = number.long_to_bytes(privkey.e)
|
|
|
|
|
pubexp = base64.urlsafe_b64encode(expBytes).decode("utf-8")
|
2019-06-28 18:55:29 +00:00
|
|
|
|
return f"data:application/magic-public-key,RSA.{mod}.{pubexp}"
|
|
|
|
|
|
2020-04-04 14:14:25 +00:00
|
|
|
|
|
|
|
|
|
def storeWebfingerEndpoint(nickname: str, domain: str, port: int,
|
|
|
|
|
baseDir: str, wfJson: {}) -> bool:
|
2019-06-28 18:55:29 +00:00
|
|
|
|
"""Stores webfinger endpoint for a user to a file
|
|
|
|
|
"""
|
2020-04-04 14:14:25 +00:00
|
|
|
|
originalDomain = domain
|
2019-08-16 20:35:11 +00:00
|
|
|
|
if port:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
if port != 80 and port != 443:
|
2019-08-16 20:35:11 +00:00
|
|
|
|
if ':' not in domain:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
domain = domain + ':' + str(port)
|
|
|
|
|
handle = nickname + '@' + domain
|
|
|
|
|
wfSubdir = '/wfendpoints'
|
|
|
|
|
if not os.path.isdir(baseDir + wfSubdir):
|
|
|
|
|
os.mkdir(baseDir + wfSubdir)
|
|
|
|
|
filename = baseDir + wfSubdir + '/' + handle.lower() + '.json'
|
|
|
|
|
saveJson(wfJson, filename)
|
|
|
|
|
if nickname == 'inbox':
|
|
|
|
|
handle = originalDomain + '@' + domain
|
|
|
|
|
filename = baseDir + wfSubdir + '/' + handle.lower() + '.json'
|
|
|
|
|
saveJson(wfJson, filename)
|
2019-06-28 18:55:29 +00:00
|
|
|
|
return True
|
|
|
|
|
|
2020-04-04 14:14:25 +00:00
|
|
|
|
|
|
|
|
|
def createWebfingerEndpoint(nickname: str, domain: str, port: int,
|
|
|
|
|
httpPrefix: str, publicKeyPem) -> {}:
|
2019-06-28 18:55:29 +00:00
|
|
|
|
"""Creates a webfinger endpoint for a user
|
|
|
|
|
"""
|
2020-04-04 14:14:25 +00:00
|
|
|
|
originalDomain = domain
|
2019-08-16 20:35:11 +00:00
|
|
|
|
if port:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
if port != 80 and port != 443:
|
2019-08-16 20:35:11 +00:00
|
|
|
|
if ':' not in domain:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
domain = domain + ':' + str(port)
|
|
|
|
|
|
|
|
|
|
personName = nickname
|
|
|
|
|
personId = httpPrefix + "://" + domain + "/users/" + personName
|
|
|
|
|
subjectStr = "acct:" + personName + "@" + originalDomain
|
|
|
|
|
profilePageHref = httpPrefix + "://" + domain + "/@" + nickname
|
|
|
|
|
if nickname == 'inbox' or nickname == originalDomain:
|
|
|
|
|
personName = 'actor'
|
|
|
|
|
personId = httpPrefix + "://" + domain + "/" + personName
|
|
|
|
|
subjectStr = "acct:" + originalDomain + "@" + originalDomain
|
|
|
|
|
profilePageHref = httpPrefix + '://' + domain + \
|
|
|
|
|
'/about/more?instance_actor=true'
|
|
|
|
|
|
|
|
|
|
actor = httpPrefix + "://" + domain + "/users/" + nickname
|
|
|
|
|
account = {
|
2019-06-28 18:55:29 +00:00
|
|
|
|
"aliases": [
|
2020-04-04 14:14:25 +00:00
|
|
|
|
httpPrefix + "://" + domain + "/@" + personName,
|
2019-08-23 20:03:06 +00:00
|
|
|
|
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
|
|
|
|
{
|
2020-04-04 14:14:25 +00:00
|
|
|
|
"href": actor + ".atom",
|
2019-08-26 14:30:09 +00:00
|
|
|
|
"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
|
|
|
|
|
|
2020-04-04 14:14:25 +00:00
|
|
|
|
|
|
|
|
|
def webfingerNodeInfo(httpPrefix: str, domainFull: str) -> {}:
|
2019-11-13 10:32:12 +00:00
|
|
|
|
""" /.well-known/nodeinfo endpoint
|
|
|
|
|
"""
|
2020-04-04 14:14:25 +00:00
|
|
|
|
nodeinfo = {
|
2019-11-13 10:32:12 +00:00
|
|
|
|
'links': [
|
|
|
|
|
{
|
2020-04-04 14:14:25 +00:00
|
|
|
|
'href': httpPrefix + '://' + domainFull + '/nodeinfo/2.0',
|
2019-11-13 10:32:12 +00:00
|
|
|
|
'rel': 'http://nodeinfo.diaspora.software/ns/schema/2.0'
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
return nodeinfo
|
|
|
|
|
|
2020-04-04 14:14:25 +00:00
|
|
|
|
|
|
|
|
|
def webfingerMeta(httpPrefix: str, domainFull: str) -> str:
|
2019-08-16 20:52:55 +00:00
|
|
|
|
"""Return /.well-known/host-meta
|
2019-06-28 18:55:29 +00:00
|
|
|
|
"""
|
2020-04-04 14:14:25 +00:00
|
|
|
|
metaStr = "<?xml version=’1.0' encoding=’UTF-8'?>"
|
|
|
|
|
metaStr += "<XRD xmlns=’http://docs.oasis-open.org/ns/xri/xrd-1.0'"
|
|
|
|
|
metaStr += " xmlns:hm=’http://host-meta.net/xrd/1.0'>"
|
|
|
|
|
metaStr += ""
|
|
|
|
|
metaStr += "<hm:Host>" + domainFull + "</hm:Host>"
|
|
|
|
|
metaStr += ""
|
|
|
|
|
metaStr += "<Link rel=’lrdd’"
|
|
|
|
|
metaStr += " template=’" + httpPrefix + "://" + domainFull + \
|
|
|
|
|
"/describe?uri={uri}'>"
|
|
|
|
|
metaStr += " <Title>Resource Descriptor</Title>"
|
|
|
|
|
metaStr += " </Link>"
|
|
|
|
|
metaStr += "</XRD>"
|
2019-11-13 10:32:12 +00:00
|
|
|
|
return metaStr
|
2019-08-16 20:52:55 +00:00
|
|
|
|
|
2020-04-04 14:14:25 +00:00
|
|
|
|
|
|
|
|
|
def webfingerLookup(path: str, baseDir: str,
|
|
|
|
|
domain: str, onionDomain: str,
|
|
|
|
|
port: int, debug: bool) -> {}:
|
2019-06-28 18:55:29 +00:00
|
|
|
|
"""Lookup the webfinger endpoint for an account
|
|
|
|
|
"""
|
2020-03-22 21:16:02 +00:00
|
|
|
|
if not path.startswith('/.well-known/webfinger?'):
|
2019-06-28 18:55:29 +00:00
|
|
|
|
return None
|
2020-04-04 14:14:25 +00:00
|
|
|
|
handle = None
|
2019-06-28 18:55:29 +00:00
|
|
|
|
if 'resource=acct:' in path:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
handle = path.split('resource=acct:')[1].strip()
|
2019-07-19 14:19:36 +00:00
|
|
|
|
if debug:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
print('DEBUG: WEBFINGER handle ' + handle)
|
2019-06-28 18:55:29 +00:00
|
|
|
|
else:
|
|
|
|
|
if 'resource=acct%3A' in path:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
handle = path.split('resource=acct%3A')[1]
|
|
|
|
|
handle = handle.replace('%40', '@', 1)
|
|
|
|
|
handle = handle.replace('%3A', ':', 1).strip()
|
2019-07-19 14:19:36 +00:00
|
|
|
|
if debug:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
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:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
handle = handle.split('&')[0].strip()
|
2019-07-19 14:19:36 +00:00
|
|
|
|
if debug:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
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:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
print('DEBUG: WEBFINGER no @ in handle ' + handle)
|
2019-06-28 18:55:29 +00:00
|
|
|
|
return None
|
2019-08-16 20:35:11 +00:00
|
|
|
|
if port:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
if port != 80 and port != 443:
|
2019-08-16 20:35:11 +00:00
|
|
|
|
if ':' not in handle:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
handle = handle + ':' + str(port)
|
2019-08-23 14:18:31 +00:00
|
|
|
|
# convert @domain@domain to inbox@domain
|
|
|
|
|
if '@' in handle:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
handleDomain = handle.split('@')[1]
|
|
|
|
|
if handle.startswith(handleDomain + '@'):
|
|
|
|
|
handle = 'inbox@' + handleDomain
|
2020-03-02 14:35:44 +00:00
|
|
|
|
# if this is a lookup for a handle using its onion domain
|
|
|
|
|
# then swap the onion domain for the clearnet version
|
2020-04-04 14:14:25 +00:00
|
|
|
|
onionify = False
|
2020-03-02 14:35:44 +00:00
|
|
|
|
if onionDomain:
|
|
|
|
|
if onionDomain in handle:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
handle = handle.replace(onionDomain, domain)
|
|
|
|
|
onionify = True
|
|
|
|
|
filename = baseDir + '/wfendpoints/' + handle.lower() + '.json'
|
2019-07-19 14:19:36 +00:00
|
|
|
|
if debug:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
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:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
print('DEBUG: WEBFINGER filename not found ' + filename)
|
2019-06-28 18:55:29 +00:00
|
|
|
|
return None
|
2020-03-02 14:35:44 +00:00
|
|
|
|
if not onionify:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
wfJson = loadJson(filename)
|
2020-03-02 14:35:44 +00:00
|
|
|
|
else:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
print('Webfinger request for onionified ' + handle)
|
|
|
|
|
wfJson = loadJsonOnionify(filename, domain, onionDomain)
|
2019-10-22 11:55:06 +00:00
|
|
|
|
if not wfJson:
|
2020-04-04 14:14:25 +00:00
|
|
|
|
wfJson = {"nickname": "unknown"}
|
2019-06-28 18:55:29 +00:00
|
|
|
|
return wfJson
|