Handle Diaspora's XML webfinger

This commit is contained in:
Andrew Godwin 2022-12-31 11:13:51 -07:00
parent 8fe99718f3
commit 13fc4b42de
3 changed files with 79 additions and 10 deletions

View File

@ -21,6 +21,7 @@ pydantic~=1.10.2
pyld~=2.0.3 pyld~=2.0.3
pylibmc~=1.6.3 pylibmc~=1.6.3
pymemcache~=4.0.0 pymemcache~=4.0.0
pytest-asyncio~=0.20.3
python-dateutil~=2.8.2 python-dateutil~=2.8.2
python-dotenv~=0.21.0 python-dotenv~=0.21.0
redis~=4.4.0 redis~=4.4.0

View File

@ -1,5 +1,6 @@
import pytest import pytest
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
from pytest_httpx import HTTPXMock
from core.models import Config from core.models import Config
from users.models import Domain, Identity, User from users.models import Domain, Identity, User
@ -176,3 +177,57 @@ def test_fetch_actor(httpx_mock, config_system):
assert identity.image_uri == "https://example.com/image.jpg" assert identity.image_uri == "https://example.com/image.jpg"
assert identity.summary == "<p>A test user</p>" assert identity.summary == "<p>A test user</p>"
assert "ts-a-faaaake" in identity.public_key assert "ts-a-faaaake" in identity.public_key
@pytest.mark.django_db
@pytest.mark.asyncio
async def test_fetch_webfinger_url(httpx_mock: HTTPXMock, config_system):
"""
Ensures that we can deal with various kinds of webfinger URLs
"""
# With no host-meta, it should be the default
assert (
await Identity.fetch_webfinger_url("example.com")
== "https://example.com/.well-known/webfinger?resource={uri}"
)
# Inject a host-meta directing it to a subdomain
httpx_mock.add_response(
url="https://example.com/.well-known/host-meta",
text="""<?xml version="1.0" encoding="UTF-8"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
<Link rel="lrdd" template="https://fedi.example.com/.well-known/webfinger?resource={uri}"/>
</XRD>""",
)
assert (
await Identity.fetch_webfinger_url("example.com")
== "https://fedi.example.com/.well-known/webfinger?resource={uri}"
)
# Inject a host-meta directing it to a different URL format
httpx_mock.add_response(
url="https://example.com/.well-known/host-meta",
text="""<?xml version="1.0" encoding="UTF-8"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
<Link rel="lrdd" template="https://example.com/amazing-webfinger?query={uri}"/>
</XRD>""",
)
assert (
await Identity.fetch_webfinger_url("example.com")
== "https://example.com/amazing-webfinger?query={uri}"
)
# Inject a host-meta directing it to a different url THAT SUPPORTS XML ONLY
# (we want to ignore that one)
httpx_mock.add_response(
url="https://example.com/.well-known/host-meta",
text="""<?xml version="1.0" encoding="UTF-8"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
<Link rel="lrdd" template="https://xmlfedi.example.com/webfinger?q={uri}" type="application/xrd+xml"/>
</XRD>""",
)
assert (
await Identity.fetch_webfinger_url("example.com")
== "https://example.com/.well-known/webfinger?resource={uri}"
)

View File

@ -601,14 +601,11 @@ class Identity(StatorModel):
### Actor/Webfinger fetching ### ### Actor/Webfinger fetching ###
@classmethod @classmethod
async def fetch_webfinger(cls, handle: str) -> tuple[str | None, str | None]: async def fetch_webfinger_url(cls, domain: str):
""" """
Given a username@domain handle, returns a tuple of Given a domain (hostname), returns the correct webfinger URL to use
(actor uri, canonical handle) or None, None if it does not resolve. based on probing host-meta.
""" """
domain = handle.split("@")[1].lower()
webfinger_url = f"https://{domain}/.well-known/webfinger?resource={{uri}}"
async with httpx.AsyncClient( async with httpx.AsyncClient(
timeout=settings.SETUP.REMOTE_TIMEOUT, timeout=settings.SETUP.REMOTE_TIMEOUT,
headers={"User-Agent": settings.TAKAHE_USER_AGENT}, headers={"User-Agent": settings.TAKAHE_USER_AGENT},
@ -626,13 +623,29 @@ class Identity(StatorModel):
if response.status_code == 200 and response.content.strip(): if response.status_code == 200 and response.content.strip():
tree = etree.fromstring(response.content) tree = etree.fromstring(response.content)
template = tree.xpath( template = tree.xpath(
"string(.//*[local-name() = 'Link' and @rel='lrdd']/@template)" "string(.//*[local-name() = 'Link' and @rel='lrdd' and (not(@type) or @type='application/jrd+json')]/@template)"
) )
if template: if template:
webfinger_url = template return template
except (httpx.RequestError, etree.ParseError): except (httpx.RequestError, etree.ParseError):
pass pass
return f"https://{domain}/.well-known/webfinger?resource={{uri}}"
@classmethod
async def fetch_webfinger(cls, handle: str) -> tuple[str | None, str | None]:
"""
Given a username@domain handle, returns a tuple of
(actor uri, canonical handle) or None, None if it does not resolve.
"""
domain = handle.split("@")[1].lower()
webfinger_url = await cls.fetch_webfinger_url(domain)
# Go make a Webfinger request
async with httpx.AsyncClient(
timeout=settings.SETUP.REMOTE_TIMEOUT,
headers={"User-Agent": settings.TAKAHE_USER_AGENT},
) as client:
try: try:
response = await client.get( response = await client.get(
webfinger_url.format(uri=f"acct:{handle}"), webfinger_url.format(uri=f"acct:{handle}"),
@ -640,12 +653,12 @@ class Identity(StatorModel):
headers={"Accept": "application/json"}, headers={"Accept": "application/json"},
) )
response.raise_for_status() response.raise_for_status()
except httpx.HTTPError as ex: except httpx.RequestError as ex:
response = getattr(ex, "response", None) response = getattr(ex, "response", None)
if ( if (
response response
and response.status_code < 500 and response.status_code < 500
and response.status_code not in [404, 410] and response.status_code not in [401, 403, 404, 410]
): ):
raise ValueError( raise ValueError(
f"Client error fetching webfinger: {response.status_code}", f"Client error fetching webfinger: {response.status_code}",