epicyon/webfinger.py

452 lines
14 KiB
Python
Raw Normal View History

2020-04-04 14:14:25 +00:00
__filename__ = "webfinger.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
2021-01-26 10:07:42 +00:00
__version__ = "1.2.0"
2020-04-04 14:14:25 +00:00
__maintainer__ = "Bob Mottram"
2021-09-10 16:14:50 +00:00
__email__ = "bob@libreserver.org"
2020-04-04 14:14:25 +00:00
__status__ = "Production"
2021-06-15 15:08:12 +00:00
__module_group__ = "ActivityPub"
2019-06-28 18:55:29 +00:00
import os
2020-04-15 11:10:30 +00:00
import urllib.parse
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
2020-12-16 11:19:16 +00:00
from utils import getFullDomain
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
2020-06-11 12:26:15 +00:00
from utils import getProtocolPrefixes
2021-06-26 14:21:24 +00:00
from utils import removeDomainPort
2021-07-29 22:41:27 +00:00
from utils import getUserPaths
2021-07-30 13:00:23 +00:00
from utils import getGroupPaths
2021-08-14 11:13:39 +00:00
from utils import localActorUrl
2019-06-28 18:55:29 +00:00
2020-04-04 14:14:25 +00:00
2021-07-30 13:00:23 +00:00
def _parseHandle(handle: str) -> (str, str, bool):
2021-07-29 22:41:27 +00:00
"""Parses a handle and returns nickname and domain
"""
2021-12-26 00:07:44 +00:00
group_account = False
2019-06-28 18:55:29 +00:00
if '.' not in handle:
2021-07-30 13:00:23 +00:00
return None, None, False
2020-06-11 12:26:15 +00:00
prefixes = getProtocolPrefixes()
2020-06-11 12:16:45 +00:00
handleStr = handle
for prefix in prefixes:
handleStr = handleStr.replace(prefix, '')
2021-07-29 22:41:27 +00:00
# try domain/@nick
2019-06-28 18:55:29 +00:00
if '/@' in handle:
2020-04-04 14:14:25 +00:00
domain, nickname = handleStr.split('/@')
2021-07-30 13:00:23 +00:00
return nickname, domain, False
2021-07-29 22:41:27 +00:00
# try nick@domain
if '@' in handle:
2021-07-30 13:00:23 +00:00
if handle.startswith('!'):
handle = handle[1:]
2021-12-26 00:07:44 +00:00
group_account = True
2021-07-29 22:41:27 +00:00
nickname, domain = handle.split('@')
2021-12-26 00:07:44 +00:00
return nickname, domain, group_account
2021-07-29 22:41:27 +00:00
# try for different /users/ paths
usersPaths = getUserPaths()
2021-07-30 13:00:23 +00:00
groupPaths = getGroupPaths()
2021-07-29 22:41:27 +00:00
for possibleUsersPath in usersPaths:
if possibleUsersPath in handle:
2021-07-30 13:00:23 +00:00
if possibleUsersPath in groupPaths:
2021-12-26 00:07:44 +00:00
group_account = True
2021-07-29 22:41:27 +00:00
domain, nickname = handleStr.split(possibleUsersPath)
2021-12-26 00:07:44 +00:00
return nickname, domain, group_account
2021-07-29 22:41:27 +00:00
2021-07-30 13:00:23 +00:00
return None, None, False
2019-06-28 18:55:29 +00:00
2021-12-25 17:09:22 +00:00
def webfingerHandle(session, handle: str, http_prefix: str,
2021-12-25 22:28:18 +00:00
cached_webfingers: {},
2021-12-25 20:34:38 +00:00
fromDomain: str, project_version: str,
2021-12-26 00:07:44 +00:00
debug: bool, group_account: bool,
2021-12-25 23:03:28 +00:00
signing_priv_key_pem: str) -> {}:
2020-06-23 10:41:12 +00:00
"""Gets webfinger result for the given ActivityPub handle
"""
2019-07-16 10:19:04 +00:00
if not session:
2021-03-14 19:22:58 +00:00
if debug:
print('WARN: No session specified for webfingerHandle')
2019-07-16 10:19:04 +00:00
return None
2019-07-19 13:32:58 +00:00
2021-07-30 13:00:23 +00:00
nickname, domain, grpAccount = _parseHandle(handle)
2019-07-03 09:40:27 +00:00
if not nickname:
2019-06-28 18:55:29 +00:00
return None
wfDomain = removeDomainPort(domain)
2021-07-29 22:48:31 +00:00
wfHandle = nickname + '@' + wfDomain
2021-12-25 22:28:18 +00:00
wf = getWebfingerFromCache(wfHandle, cached_webfingers)
2019-06-30 15:03:26 +00:00
if wf:
2021-03-14 19:22:58 +00:00
if debug:
print('Webfinger from cache: ' + str(wf))
2020-03-22 21:16:02 +00:00
return wf
2021-12-25 17:09:22 +00:00
url = '{}://{}/.well-known/webfinger'.format(http_prefix, domain)
2020-04-04 14:14:25 +00:00
hdr = {
2020-03-22 20:36:19 +00:00
'Accept': 'application/jrd+json'
}
par = {
'resource': 'acct:{}'.format(wfHandle)
}
2019-07-04 17:31:41 +00:00
try:
2020-04-04 14:14:25 +00:00
result = \
2021-12-25 23:03:28 +00:00
getJson(signing_priv_key_pem, session, url, hdr, par,
2021-12-25 20:34:38 +00:00
debug, project_version, http_prefix, fromDomain)
2021-12-25 15:28:52 +00:00
except Exception as ex:
print('ERROR: webfingerHandle ' + str(ex))
2021-07-30 13:00:23 +00:00
return None
if result:
2021-12-25 22:28:18 +00:00
storeWebfingerInCache(wfHandle, result, cached_webfingers)
2020-05-07 13:26:55 +00:00
else:
2021-03-14 19:22:58 +00:00
if debug:
print("WARN: Unable to webfinger " + url + ' ' +
'nickname: ' + str(nickname) + ' ' +
'domain: ' + str(wfDomain) + ' ' +
'headers: ' + str(hdr) + ' ' +
'params: ' + str(par))
2020-05-07 13:26:55 +00:00
2019-06-28 18:55:29 +00:00
return result
2020-04-04 14:14:25 +00:00
def storeWebfingerEndpoint(nickname: str, domain: str, port: int,
2021-12-25 16:17:53 +00:00
base_dir: 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
2020-12-16 11:19:16 +00:00
domain = getFullDomain(domain, port)
2020-04-04 14:14:25 +00:00
handle = nickname + '@' + domain
wfSubdir = '/wfendpoints'
2021-12-25 16:17:53 +00:00
if not os.path.isdir(base_dir + wfSubdir):
os.mkdir(base_dir + wfSubdir)
filename = base_dir + wfSubdir + '/' + handle + '.json'
2020-04-04 14:14:25 +00:00
saveJson(wfJson, filename)
if nickname == 'inbox':
handle = originalDomain + '@' + domain
2021-12-25 16:17:53 +00:00
filename = base_dir + wfSubdir + '/' + handle + '.json'
2020-04-04 14:14:25 +00:00
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,
2021-12-25 17:09:22 +00:00
http_prefix: str, publicKeyPem: str,
2021-12-26 00:07:44 +00:00
group_account: bool) -> {}:
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
2020-12-16 11:19:16 +00:00
domain = getFullDomain(domain, port)
2020-04-04 14:14:25 +00:00
personName = nickname
2021-12-25 17:09:22 +00:00
personId = localActorUrl(http_prefix, personName, domain)
subjectStr = "acct:" + personName + "@" + originalDomain
2021-12-25 17:09:22 +00:00
profilePageHref = http_prefix + "://" + domain + "/@" + nickname
2020-04-04 14:14:25 +00:00
if nickname == 'inbox' or nickname == originalDomain:
personName = 'actor'
2021-12-25 17:09:22 +00:00
personId = http_prefix + "://" + domain + "/" + personName
2020-04-04 14:14:25 +00:00
subjectStr = "acct:" + originalDomain + "@" + originalDomain
2021-12-25 17:09:22 +00:00
profilePageHref = http_prefix + '://' + domain + \
2020-04-04 14:14:25 +00:00
'/about/more?instance_actor=true'
2021-12-25 17:09:22 +00:00
personLink = http_prefix + "://" + domain + "/@" + personName
2020-04-04 14:14:25 +00:00
account = {
2019-06-28 18:55:29 +00:00
"aliases": [
2021-10-28 09:34:11 +00:00
personLink,
2019-08-23 20:03:06 +00:00
personId
2019-06-28 18:55:29 +00:00
],
"links": [
2021-10-28 09:33:27 +00:00
{
2021-10-28 09:41:18 +00:00
"href": personLink + "/avatar.png",
2021-10-28 09:33:27 +00:00
"rel": "http://webfinger.net/rel/avatar",
"type": "image/png"
},
2021-10-28 09:41:18 +00:00
{
2021-12-25 17:09:22 +00:00
"href": http_prefix + "://" + domain + "/blog/" + personName,
2021-10-28 09:41:18 +00:00
"rel": "http://webfinger.net/rel/blog"
},
2019-06-28 18:55:29 +00:00
{
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-23 20:03:06 +00:00
"href": personId,
2019-06-28 18:55:29 +00:00
"rel": "self",
"type": "application/activity+json"
}
],
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
2021-12-26 10:00:46 +00:00
def webfingerNodeInfo(http_prefix: str, domain_full: 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': [
{
2021-12-26 10:00:46 +00:00
'href': http_prefix + '://' + domain_full + '/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
2021-12-26 10:00:46 +00:00
def webfingerMeta(http_prefix: str, domain_full: str) -> str:
2019-08-16 20:52:55 +00:00
"""Return /.well-known/host-meta
2019-06-28 18:55:29 +00:00
"""
2021-07-06 12:50:38 +00:00
metaStr = \
"<?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'>" + \
"" + \
2021-12-26 10:00:46 +00:00
"<hm:Host>" + domain_full + "</hm:Host>" + \
2021-07-06 12:50:38 +00:00
"" + \
"<Link rel=lrdd" + \
2021-12-26 10:00:46 +00:00
" template=" + http_prefix + "://" + domain_full + \
2021-07-06 12:50:38 +00:00
"/describe?uri={uri}'>" + \
" <Title>Resource Descriptor</Title>" + \
" </Link>" + \
"</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
2021-12-25 16:17:53 +00:00
def webfingerLookup(path: str, base_dir: str,
2021-12-25 20:43:43 +00:00
domain: str, onion_domain: str,
2020-04-04 14:14:25 +00:00
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
resType = 'acct'
if 'resource=' + resType + ':' in path:
handle = path.split('resource=' + resType + ':')[1].strip()
handle = urllib.parse.unquote(handle)
if debug:
print('DEBUG: WEBFINGER handle ' + handle)
elif 'resource=' + resType + '%3A' in path:
handle = path.split('resource=' + resType + '%3A')[1]
handle = urllib.parse.unquote(handle.strip())
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:
2020-04-04 14:14:25 +00:00
handle = handle.split('&')[0].strip()
2021-10-28 13:27: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
2020-12-16 11:19:16 +00:00
handle = getFullDomain(handle, 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
2021-12-25 20:43:43 +00:00
if onion_domain:
if onion_domain in handle:
handle = handle.replace(onion_domain, domain)
2020-04-04 14:14:25 +00:00
onionify = True
2021-08-31 19:04:29 +00:00
# instance actor
if handle.startswith('actor@'):
handle = handle.replace('actor@', 'inbox@', 1)
elif handle.startswith('Actor@'):
handle = handle.replace('Actor@', 'inbox@', 1)
2021-12-25 16:17:53 +00:00
filename = base_dir + '/wfendpoints/' + handle + '.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)
2021-12-25 20:43:43 +00:00
wfJson = loadJsonOnionify(filename, domain, onion_domain)
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
def _webfingerUpdateAvatar(wfJson: {}, actorJson: {}) -> bool:
"""Updates the avatar image link
"""
found = False
avatarUrl = actorJson['icon']['url']
mediaType = actorJson['icon']['mediaType']
for link in wfJson['links']:
if not link.get('rel'):
continue
if not link['rel'].endswith('://webfinger.net/rel/avatar'):
continue
found = True
if link['href'] != avatarUrl or link['type'] != mediaType:
link['href'] = avatarUrl
link['type'] = mediaType
return True
break
if found:
return False
wfJson['links'].append({
"href": avatarUrl,
"rel": "http://webfinger.net/rel/avatar",
"type": mediaType
})
return True
2021-10-28 10:21:28 +00:00
def _webfingerAddBlogLink(wfJson: {}, actorJson: {}) -> bool:
"""Adds a blog link to webfinger if needed
"""
found = False
if '/users/' in actorJson['id']:
blogUrl = \
actorJson['id'].split('/users/')[0] + '/blog/' + \
actorJson['id'].split('/users/')[1]
else:
blogUrl = \
actorJson['id'].split('/@')[0] + '/blog/' + \
actorJson['id'].split('/@')[1]
for link in wfJson['links']:
if not link.get('rel'):
continue
if not link['rel'].endswith('://webfinger.net/rel/blog'):
continue
found = True
if link['href'] != blogUrl:
link['href'] = blogUrl
return True
break
if found:
return False
wfJson['links'].append({
"href": blogUrl,
"rel": "http://webfinger.net/rel/blog"
})
return True
def _webfingerUpdateFromProfile(wfJson: {}, actorJson: {}) -> bool:
"""Updates webfinger Email/blog/xmpp links from profile
Returns true if one or more tags has been changed
"""
if not actorJson.get('attachment'):
return False
changed = False
webfingerPropertyName = {
"xmpp": "xmpp",
2020-05-04 14:10:27 +00:00
"matrix": "matrix",
"email": "mailto",
"ssb": "ssb",
2021-07-06 12:53:10 +00:00
"briar": "briar",
"cwtch": "cwtch",
"jami": "jami",
"tox": "toxId"
}
2021-07-06 13:11:00 +00:00
aliasesNotFound = []
2021-07-06 13:17:38 +00:00
for name, alias in webfingerPropertyName.items():
aliasesNotFound.append(alias)
2021-07-06 13:11:00 +00:00
for propertyValue in actorJson['attachment']:
if not propertyValue.get('name'):
continue
propertyName = propertyValue['name'].lower()
2021-07-06 12:50:38 +00:00
found = False
2021-07-06 12:58:21 +00:00
for name, alias in webfingerPropertyName.items():
2021-07-06 12:50:38 +00:00
if name == propertyName:
2021-07-06 13:17:38 +00:00
if alias in aliasesNotFound:
aliasesNotFound.remove(alias)
2021-07-06 12:50:38 +00:00
found = True
break
if not found:
continue
if not propertyValue.get('type'):
continue
if not propertyValue.get('value'):
continue
if propertyValue['type'] != 'PropertyValue':
continue
newValue = propertyValue['value'].strip()
2021-07-06 13:02:00 +00:00
if '://' in newValue:
newValue = newValue.split('://')[1]
aliasIndex = 0
found = False
for alias in wfJson['aliases']:
if alias.startswith(webfingerPropertyName[propertyName] + ':'):
found = True
break
aliasIndex += 1
newAlias = webfingerPropertyName[propertyName] + ':' + newValue
if found:
if wfJson['aliases'][aliasIndex] != newAlias:
changed = True
wfJson['aliases'][aliasIndex] = newAlias
else:
wfJson['aliases'].append(newAlias)
changed = True
2021-07-06 13:11:00 +00:00
# remove any aliases which are no longer in the actor profile
removeAlias = []
2021-07-06 13:17:38 +00:00
for alias in aliasesNotFound:
for fullAlias in wfJson['aliases']:
if fullAlias.startswith(alias + ':'):
removeAlias.append(fullAlias)
for fullAlias in removeAlias:
wfJson['aliases'].remove(fullAlias)
2021-07-06 13:11:00 +00:00
changed = True
if _webfingerUpdateAvatar(wfJson, actorJson):
2021-10-28 10:21:28 +00:00
changed = True
if _webfingerAddBlogLink(wfJson, actorJson):
changed = True
return changed
2021-12-25 16:17:53 +00:00
def webfingerUpdate(base_dir: str, nickname: str, domain: str,
2021-12-25 20:43:43 +00:00
onion_domain: str,
2021-12-25 22:28:18 +00:00
cached_webfingers: {}) -> None:
handle = nickname + '@' + domain
wfSubdir = '/wfendpoints'
2021-12-25 16:17:53 +00:00
if not os.path.isdir(base_dir + wfSubdir):
return
2021-12-25 16:17:53 +00:00
filename = base_dir + wfSubdir + '/' + handle + '.json'
onionify = False
2021-12-25 20:43:43 +00:00
if onion_domain:
if onion_domain in handle:
handle = handle.replace(onion_domain, domain)
onionify = True
if not onionify:
wfJson = loadJson(filename)
else:
2021-12-25 20:43:43 +00:00
wfJson = loadJsonOnionify(filename, domain, onion_domain)
if not wfJson:
return
2021-12-25 16:17:53 +00:00
actorFilename = base_dir + '/accounts/' + handle + '.json'
actorJson = loadJson(actorFilename)
if not actorJson:
return
if _webfingerUpdateFromProfile(wfJson, actorJson):
if saveJson(wfJson, filename):
2021-12-25 22:28:18 +00:00
storeWebfingerInCache(handle, wfJson, cached_webfingers)