Reviewed-on: 
Co-authored-by: Joel Beckmeyer <joel@beckmeyer.us>
Co-committed-by: Joel Beckmeyer <joel@beckmeyer.us>
This commit is contained in:
Joel Beckmeyer 2022-08-02 13:54:22 +00:00 committed by floatingghost
parent c3eea8dc7d
commit e26388a01c
20 changed files with 489 additions and 65 deletions

View file

@ -173,6 +173,7 @@ you might end up in a situation where you don't have an ability to get it.
- Attachment dimensions and blurhashes are federated when available. - Attachment dimensions and blurhashes are federated when available.
- Mastodon API: support `poll` notification. - Mastodon API: support `poll` notification.
- Pinned posts federation - Pinned posts federation
- Possibility to discover users like `user@example.org`, while Akkoma is working on `akkoma.example.org`. Additional configuration required.
### Fixed ### Fixed
- Don't crash so hard when email settings are invalid. - Don't crash so hard when email settings are invalid.

View file

@ -809,6 +809,8 @@
{Pleroma.Search, [max_running: 30, max_waiting: 50]} {Pleroma.Search, [max_running: 30, max_waiting: 50]}
] ]
config :pleroma, Pleroma.Web.WebFinger, domain: nil, update_nickname_on_user_fetch: true
config :pleroma, Pleroma.Search, module: Pleroma.Search.DatabaseSearch config :pleroma, Pleroma.Search, module: Pleroma.Search.DatabaseSearch
config :pleroma, Pleroma.Search.Meilisearch, config :pleroma, Pleroma.Search.Meilisearch,

View file

@ -126,6 +126,8 @@
config :pleroma, :cachex, provider: Pleroma.CachexMock config :pleroma, :cachex, provider: Pleroma.CachexMock
config :pleroma, Pleroma.Web.WebFinger, update_nickname_on_user_fetch: false
config :pleroma, :side_effects, config :pleroma, :side_effects,
ap_streamer: Pleroma.Web.ActivityPub.ActivityPubMock, ap_streamer: Pleroma.Web.ActivityPub.ActivityPubMock,
logger: Pleroma.LoggerMock logger: Pleroma.LoggerMock

View file

@ -0,0 +1,62 @@
# How to use a different domain name for Akkoma and the users it serves
Akkoma users are primarily identified by a `user@example.org` handle, and you might want this identifier to be the same as your email or jabber account, for instance.
However, in this case, you are almost certainly serving some web content on `https://example.org` already, and you might want to use another domain (say `akkoma.example.org`) for Akkoma itself.
Akkoma supports that, but it might be tricky to set up, and any error might prevent you from federating with other instances.
*If you are already running Akkoma on `example.org`, it is no longer possible to move it to `akkoma.example.org`.*
## Account identifiers
It is important to understand that for federation purposes, a user in Akkoma has two unique identifiers associated:
- A webfinger `acct:` URI, used for discovery and as a verifiable global name for the user across Akkoma instances. In our example, our account's acct: URI is `acct:user@example.org`
- An author/actor URI, used in every other aspect of federation. This is the way in which users are identified in ActivityPub, the underlying protocol used for federation with other Akkoma instances.
In our case, it is `https://akkoma.example.org/users/user`.
Both account identifiers are unique and required for Akkoma. An important risk if you set up your Akkoma instance incorrectly is to create two users (with different acct: URIs) with conflicting author/actor URIs.
## WebFinger
As said earlier, each Akkoma user has an `acct`: URI, which is used for discovery and authentication. When you add @user@example.org, a webfinger query is performed. This is done in two steps:
1. Querying `https://example.org/.well-known/host-meta` (where the domain of the URL matches the domain part of the `acct`: URI) to get information on how to perform the query.
This file will indeed contain a URL template of the form `https://example.org/.well-known/webfinger?resource={uri}` that will be used in the second step.
2. Fill the returned template with the `acct`: URI to be queried and perform the query: `https://example.org/.well-known/webfinger?resource=acct:user@example.org`
## Configuring your Akkoma instance
**_DO NOT ATTEMPT TO CONFIGURE YOUR INSTANCE THIS WAY IF YOU DID NOT UNDERSTAND THE ABOVE_**
### Configuring Akkoma
Akkoma has a two configuration settings to enable using different domains for your users and Akkoma itself. `host` in `Pleroma.Web.Endpoint` and `domain` in `Pleroma.Web.WebFinger`. When the latter is not set, it defaults to the value of `host`.
*Be extra careful when configuring your Akkoma instance, as changing `host` may cause remote instances to register different accounts with the same author/actor URI, which will result in federation issues!*
```elixir
config :pleroma, Pleroma.Web.Endpoint,
url: [host: "pleroma.example.org"]
config :pleroma, Pleroma.Web.WebFinger, domain: "example.org"
```
- `domain` - is the domain for which your Akkoma instance has authority, it's the domain used in `acct:` URI. In our example, `domain` would be set to `example.org`.
- `host` - is the domain used for any URL generated for your instance, including the author/actor URL's. In our case, that would be `akkoma.example.org`.
### Configuring WebFinger domain
Now, you have Akkoma running at `https://akkoma.example.org` as well as a website at `https://example.org`. If you recall how webfinger queries work, the first step is to query `https://example.org/.well-known/host-meta`, which will contain an URL template.
Therefore, the easiest way to configure `example.org` is to redirect `/.well-known/host-meta` to `akkoma.example.org`.
With nginx, it would be as simple as adding:
```nginx
location = /.well-known/host-meta {
return 301 https://akkoma.example.org$request_uri;
}
```
in example.org's server block.

View file

@ -83,4 +83,13 @@ defp build_request(method, headers, options, url, body, params) do
|> Builder.add_param(:query, :query, params) |> Builder.add_param(:query, :query, params)
|> Builder.convert_to_keyword() |> Builder.convert_to_keyword()
end end
defp adapter_middlewares(_) do
if Pleroma.Config.get(:env) == :test do
# Emulate redirects in test env, which are handled by adapters in other environments
[Tesla.Middleware.FollowRedirects]
else
[]
end
end
end end

View file

@ -1437,7 +1437,7 @@ defp normalize_image(%{"url" => url}) do
defp normalize_image(urls) when is_list(urls), do: urls |> List.first() |> normalize_image() defp normalize_image(urls) when is_list(urls), do: urls |> List.first() |> normalize_image()
defp normalize_image(_), do: nil defp normalize_image(_), do: nil
defp object_to_user_data(data) do defp object_to_user_data(data, additional) do
fields = fields =
data data
|> Map.get("attachment", []) |> Map.get("attachment", [])
@ -1467,18 +1467,18 @@ defp object_to_user_data(data) do
public_key = public_key =
if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do
data["publicKey"]["publicKeyPem"] data["publicKey"]["publicKeyPem"]
else
nil
end end
shared_inbox = shared_inbox =
if is_map(data["endpoints"]) && is_binary(data["endpoints"]["sharedInbox"]) do if is_map(data["endpoints"]) && is_binary(data["endpoints"]["sharedInbox"]) do
data["endpoints"]["sharedInbox"] data["endpoints"]["sharedInbox"]
else
nil
end end
user_data = %{ # if WebFinger request was already done, we probably have acct, otherwise
# we request WebFinger here
nickname = additional[:nickname_from_acct] || generate_nickname(data)
%{
ap_id: data["id"], ap_id: data["id"],
uri: get_actor_url(data["url"]), uri: get_actor_url(data["url"]),
ap_enabled: true, ap_enabled: true,
@ -1499,21 +1499,27 @@ defp object_to_user_data(data) do
public_key: public_key, public_key: public_key,
inbox: data["inbox"], inbox: data["inbox"],
shared_inbox: shared_inbox, shared_inbox: shared_inbox,
pinned_objects: pinned_objects pinned_objects: pinned_objects,
nickname: nickname
} }
end
# nickname can be nil because of virtual actors defp generate_nickname(%{"preferredUsername" => username} = data) when is_binary(username) do
if data["preferredUsername"] do generated = "#{username}@#{URI.parse(data["id"]).host}"
Map.put(
user_data, if Config.get([WebFinger, :update_nickname_on_user_fetch]) do
:nickname, case WebFinger.finger(generated) do
"#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}" {:ok, %{"subject" => "acct:" <> acct}} -> acct
) _ -> generated
end
else else
Map.put(user_data, :nickname, nil) generated
end end
end end
# nickname can be nil because of virtual actors
defp generate_nickname(_), do: nil
def fetch_follow_information_for_user(user) do def fetch_follow_information_for_user(user) do
with {:ok, following_data} <- with {:ok, following_data} <-
Fetcher.fetch_and_contain_remote_object_from_id(user.following_address), Fetcher.fetch_and_contain_remote_object_from_id(user.following_address),
@ -1585,17 +1591,17 @@ defp collection_private(%{"first" => first}) do
defp collection_private(_data), do: {:ok, true} defp collection_private(_data), do: {:ok, true}
def user_data_from_user_object(data) do def user_data_from_user_object(data, additional \\ []) do
with {:ok, data} <- MRF.filter(data) do with {:ok, data} <- MRF.filter(data) do
{:ok, object_to_user_data(data)} {:ok, object_to_user_data(data, additional)}
else else
e -> {:error, e} e -> {:error, e}
end end
end end
def fetch_and_prepare_user_from_ap_id(ap_id) do def fetch_and_prepare_user_from_ap_id(ap_id, additional \\ []) do
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id), with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id),
{:ok, data} <- user_data_from_user_object(data) do {:ok, data} <- user_data_from_user_object(data, additional) do
{:ok, maybe_update_follow_information(data)} {:ok, maybe_update_follow_information(data)}
else else
# If this has been deleted, only log a debug and not an error # If this has been deleted, only log a debug and not an error
@ -1693,13 +1699,13 @@ def pinned_fetch_task(%{pinned_objects: pins}) do
end end
end end
def make_user_from_ap_id(ap_id) do def make_user_from_ap_id(ap_id, additional \\ []) do
user = User.get_cached_by_ap_id(ap_id) user = User.get_cached_by_ap_id(ap_id)
if user && !User.ap_enabled?(user) do if user && !User.ap_enabled?(user) do
Transmogrifier.upgrade_user_from_ap_id(ap_id) Transmogrifier.upgrade_user_from_ap_id(ap_id)
else else
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, additional) do
{:ok, _pid} = Task.start(fn -> pinned_fetch_task(data) end) {:ok, _pid} = Task.start(fn -> pinned_fetch_task(data) end)
if user do if user do
@ -1719,8 +1725,9 @@ def make_user_from_ap_id(ap_id) do
end end
def make_user_from_nickname(nickname) do def make_user_from_nickname(nickname) do
with {:ok, %{"ap_id" => ap_id}} when not is_nil(ap_id) <- WebFinger.finger(nickname) do with {:ok, %{"ap_id" => ap_id, "subject" => "acct:" <> acct}} when not is_nil(ap_id) <-
make_user_from_ap_id(ap_id) WebFinger.finger(nickname) do
make_user_from_ap_id(ap_id, nickname_from_acct: acct)
else else
_e -> {:error, "No AP id in WebFinger"} _e -> {:error, "No AP id in WebFinger"}
end end

View file

@ -32,7 +32,13 @@ def host_meta do
def webfinger(resource, fmt) when fmt in ["XML", "JSON"] do def webfinger(resource, fmt) when fmt in ["XML", "JSON"] do
host = Pleroma.Web.Endpoint.host() host = Pleroma.Web.Endpoint.host()
regex = ~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@#{host}/
regex =
if webfinger_domain = Pleroma.Config.get([__MODULE__, :domain]) do
~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@(#{host}|#{webfinger_domain})/
else
~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@#{host}/
end
with %{"username" => username} <- Regex.named_captures(regex, resource), with %{"username" => username} <- Regex.named_captures(regex, resource),
%User{} = user <- User.get_cached_by_nickname(username) do %User{} = user <- User.get_cached_by_nickname(username) do
@ -66,7 +72,7 @@ def represent_user(user, "JSON") do
{:ok, user} = User.ensure_keys_present(user) {:ok, user} = User.ensure_keys_present(user)
%{ %{
"subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}", "subject" => "acct:#{user.nickname}@#{domain()}",
"aliases" => gather_aliases(user), "aliases" => gather_aliases(user),
"links" => gather_links(user) "links" => gather_links(user)
} }
@ -88,12 +94,16 @@ def represent_user(user, "XML") do
:XRD, :XRD,
%{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"}, %{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
[ [
{:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}"} {:Subject, "acct:#{user.nickname}@#{domain()}"}
] ++ aliases ++ links ] ++ aliases ++ links
} }
|> XmlBuilder.to_doc() |> XmlBuilder.to_doc()
end end
defp domain do
Pleroma.Config.get([__MODULE__, :domain]) || Pleroma.Web.Endpoint.host()
end
defp webfinger_from_xml(body) do defp webfinger_from_xml(body) do
with {:ok, doc} <- XML.parse_document(body) do with {:ok, doc} <- XML.parse_document(body) do
subject = XML.string_from_xpath("//Subject", doc) subject = XML.string_from_xpath("//Subject", doc)
@ -150,17 +160,15 @@ def get_template_from_xml(body) do
end end
def find_lrdd_template(domain) do def find_lrdd_template(domain) do
with {:ok, %{status: status, body: body}} when status in 200..299 <- # WebFinger is restricted to HTTPS - https://tools.ietf.org/html/rfc7033#section-9.1
HTTP.get("http://#{domain}/.well-known/host-meta") do meta_url = "https://#{domain}/.well-known/host-meta"
with {:ok, %{status: status, body: body}} when status in 200..299 <- HTTP.get(meta_url) do
get_template_from_xml(body) get_template_from_xml(body)
else else
_ -> error ->
with {:ok, %{body: body, status: status}} when status in 200..299 <- Logger.warn("Can't find LRDD template in #{inspect(meta_url)}: #{inspect(error)}")
HTTP.get("https://#{domain}/.well-known/host-meta") do {:error, :lrdd_not_found}
get_template_from_xml(body)
else
e -> {:error, "Can't find LRDD template: #{inspect(e)}"}
end
end end
end end
@ -174,7 +182,7 @@ defp get_address_from_domain(domain, encoded_account) when is_binary(domain) do
end end
end end
defp get_address_from_domain(_, _), do: nil defp get_address_from_domain(_, _), do: {:error, :webfinger_no_domain}
@spec finger(String.t()) :: {:ok, map()} | {:error, any()} @spec finger(String.t()) :: {:ok, map()} | {:error, any()}
def finger(account) do def finger(account) do
@ -191,13 +199,11 @@ def finger(account) do
encoded_account = URI.encode("acct:#{account}") encoded_account = URI.encode("acct:#{account}")
with address when is_binary(address) <- get_address_from_domain(domain, encoded_account), with address when is_binary(address) <- get_address_from_domain(domain, encoded_account),
response <- {:ok, %{status: status, body: body, headers: headers}} when status in 200..299 <-
HTTP.get( HTTP.get(
address, address,
[{"accept", "application/xrd+xml,application/jrd+json"}] [{"accept", "application/xrd+xml,application/jrd+json"}]
), ) do
{:ok, %{status: status, body: body, headers: headers}} when status in 200..299 <-
response do
case List.keyfind(headers, "content-type", 0) do case List.keyfind(headers, "content-type", 0) do
{_, content_type} -> {_, content_type} ->
case Plug.Conn.Utils.media_type(content_type) do case Plug.Conn.Utils.media_type(content_type) do
@ -215,10 +221,9 @@ def finger(account) do
{:error, {:content_type, nil}} {:error, {:content_type, nil}}
end end
else else
e -> error ->
Logger.debug(fn -> "Couldn't finger #{account}" end) Logger.debug("Couldn't finger #{account}: #{inspect(error)}")
Logger.debug(fn -> inspect(e) end) error
{:error, e}
end end
end end
end end

View file

@ -1,2 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><hm:Host xmlns:hm="http://host-meta.net/xrd/1.0">framatube.org</hm:Host><Link rel="lrdd" template="http://framatube.org/main/xrd?uri={uri}"><Title>Resource Descriptor</Title></Link></XRD> <XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><hm:Host xmlns:hm="http://host-meta.net/xrd/1.0">framatube.org</hm:Host><Link rel="lrdd" template="https://framatube.org/main/xrd?uri={uri}"><Title>Resource Descriptor</Title></Link></XRD>

View file

@ -1,2 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><hm:Host xmlns:hm="http://host-meta.net/xrd/1.0">status.alpicola.com</hm:Host><Link rel="lrdd" template="http://status.alpicola.com/main/xrd?uri={uri}"><Title>Resource Descriptor</Title></Link></XRD> <XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><hm:Host xmlns:hm="http://host-meta.net/xrd/1.0">status.alpicola.com</hm:Host><Link rel="lrdd" template="https://status.alpicola.com/main/xrd?uri={uri}"><Title>Resource Descriptor</Title></Link></XRD>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
<Link rel="lrdd" template="https://{{domain}}/.well-known/webfinger?resource={uri}"/>
</XRD>

92
test/fixtures/webfinger/masto-user.json vendored Normal file
View file

@ -0,0 +1,92 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"toot": "http://joinmastodon.org/ns#",
"featured": {
"@id": "toot:featured",
"@type": "@id"
},
"featuredTags": {
"@id": "toot:featuredTags",
"@type": "@id"
},
"alsoKnownAs": {
"@id": "as:alsoKnownAs",
"@type": "@id"
},
"movedTo": {
"@id": "as:movedTo",
"@type": "@id"
},
"schema": "http://schema.org#",
"PropertyValue": "schema:PropertyValue",
"value": "schema:value",
"IdentityProof": "toot:IdentityProof",
"discoverable": "toot:discoverable",
"Device": "toot:Device",
"Ed25519Signature": "toot:Ed25519Signature",
"Ed25519Key": "toot:Ed25519Key",
"Curve25519Key": "toot:Curve25519Key",
"EncryptedMessage": "toot:EncryptedMessage",
"publicKeyBase64": "toot:publicKeyBase64",
"deviceId": "toot:deviceId",
"claim": {
"@type": "@id",
"@id": "toot:claim"
},
"fingerprintKey": {
"@type": "@id",
"@id": "toot:fingerprintKey"
},
"identityKey": {
"@type": "@id",
"@id": "toot:identityKey"
},
"devices": {
"@type": "@id",
"@id": "toot:devices"
},
"messageFranking": "toot:messageFranking",
"messageType": "toot:messageType",
"cipherText": "toot:cipherText",
"suspended": "toot:suspended",
"focalPoint": {
"@container": "@list",
"@id": "toot:focalPoint"
}
}
],
"id": "https://{{domain}}/users/{{nickname}}",
"type": "Person",
"following": "https://{{domain}}/users/{{nickname}}/following",
"followers": "https://{{domain}}/users/{{nickname}}/followers",
"inbox": "https://{{domain}}/users/{{nickname}}/inbox",
"outbox": "https://{{domain}}/users/{{nickname}}/outbox",
"featured": "https://{{domain}}/users/{{nickname}}/collections/featured",
"featuredTags": "https://{{domain}}/users/{{nickname}}/collections/tags",
"preferredUsername": "{{nickname}}",
"name": "Name Name",
"summary": "<p>Summary</p>",
"url": "https://{{domain}}/@{{nickname}}",
"manuallyApprovesFollowers": false,
"discoverable": false,
"devices": "https://{{domain}}/users/{{nickname}}/collections/devices",
"publicKey": {
"id": "https://{{domain}}/users/{{nickname}}#main-key",
"owner": "https://{{domain}}/users/{{nickname}}",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvwDujxmxoYHs64MyVB3L\nG5ZyBxV3ufaMRBFu42bkcTpISq1WwZ+3Zb6CI8zOO+nM+Q2llrVRYjZa4ZFnOLvM\nTq/Kf+Zf5wy2aCRer88gX+MsJOAtItSi412y0a/rKOuFaDYLOLeTkRvmGLgZWbsr\nZJOp+YWb3zQ5qsIOInkc5BwI172tMsGeFtsnbNApPV4lrmtTGaJ8RiM8MR7XANBO\nfOHggSt1+eAIKGIsCmINEMzs1mG9D75xKtC/sM8GfbvBclQcBstGkHAEj1VHPW0c\nh6Bok5/QQppicyb8UA1PAA9bznSFtKlYE4xCH8rlCDSDTBRtdnBWHKcj619Ujz4Q\nawIDAQAB\n-----END PUBLIC KEY-----\n"
},
"tag": [],
"attachment": [],
"endpoints": {
"sharedInbox": "https://{{domain}}/inbox"
},
"icon": {
"type": "Image",
"mediaType": "image/jpeg",
"url": "https://s3.wasabisys.com/merp/accounts/avatars/000/000/001/original/6fdd3eee632af247.jpg"
}
}

View file

@ -0,0 +1,23 @@
{
"subject": "acct:{{nickname}}@{{domain}}",
"aliases": [
"https://{{subdomain}}/@{{nickname}}",
"https://{{subdomain}}/users/{{nickname}}"
],
"links": [
{
"rel": "http://webfinger.net/rel/profile-page",
"type": "text/html",
"href": "https://{{subdomain}}/@{{nickname}}"
},
{
"rel": "self",
"type": "application/activity+json",
"href": "https://{{subdomain}}/users/{{nickname}}"
},
{
"rel": "http://ostatus.org/schema/1.0/subscribe",
"template": "https://{{subdomain}}/authorize_interaction?uri={uri}"
}
]
}

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" template="https://{{domain}}/.well-known/webfinger?resource={uri}" type="application/xrd+xml" /></XRD>

View file

@ -0,0 +1,58 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://{{domain}}/schemas/litepub-0.1.jsonld",
{
"@language": "und"
}
],
"alsoKnownAs": [],
"attachment": [],
"capabilities": {
"acceptsChatMessages": true
},
"discoverable": true,
"endpoints": {
"oauthAuthorizationEndpoint": "https://{{domain}}/oauth/authorize",
"oauthRegistrationEndpoint": "https://{{domain}}/api/v1/apps",
"oauthTokenEndpoint": "https://{{domain}}/oauth/token",
"sharedInbox": "https://{{domain}}/inbox",
"uploadMedia": "https://{{domain}}/api/ap/upload_media"
},
"followers": "https://{{domain}}/users/{{nickname}}/followers",
"following": "https://{{domain}}/users/{{nickname}}/following",
"icon": {
"type": "Image",
"url": "https://{{domain}}/media/a932a27f158b63c3a97e3a57d5384f714a82249274c6fc66c9eca581b4fd8af2.jpg"
},
"id": "https://{{domain}}/users/{{nickname}}",
"image": {
"type": "Image",
"url": "https://{{domain}}/media/db15f476d0ad14488db4762b7800479e6ef67b1824f8b9ea5c1fa05b7525c5b7.jpg"
},
"inbox": "https://{{domain}}/users/{{nickname}}/inbox",
"manuallyApprovesFollowers": false,
"name": "{{nickname}} :verified:",
"outbox": "https://{{domain}}/users/{{nickname}}/outbox",
"preferredUsername": "{{nickname}}",
"publicKey": {
"id": "https://{{domain}}/users/{{nickname}}#main-key",
"owner": "https://{{domain}}/users/{{nickname}}",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu4XOAopC4nRIxNlHlt60\n//nCicuedu5wvLGIoQ+KUM2u7/PhLrrTDEqr1A7yQL95S0X8ryYtALgFLI5A54ww\nqjMIbIGAs44lEmDLMEd+XI+XxREE8wdsFpb4QQzWug0DTyqlMouTU25k0tfKh1rF\n4PMJ3uBSjDTAGgFvLNyFWTiVVgChbTNgGOmrEBucRl4NmKzQ69/FIUwENV88oQSU\n3bWvQTEH9rWH1rCLpkmQwdRiWfnhFX/4EUqXukfgoskvenKR8ff3nYhElDqFoE0e\nqUnIW1OZceyl8JewVLcL6m0/wdKeosTsfrcWc8DKfnRYQcBGNoBEq9GrOHDU0q2v\nyQIDAQAB\n-----END PUBLIC KEY-----\n\n"
},
"summary": "Pleroma BE dev",
"tag": [
{
"icon": {
"type": "Image",
"url": "https://{{domain}}/emoji/mine/6143373a807b1ae7.png"
},
"id": "https://{{domain}}/emoji/mine/6143373a807b1ae7.png",
"name": ":verified:",
"type": "Emoji",
"updated": "1970-01-01T00:00:00Z"
}
],
"type": "Person",
"url": "https://{{domain}}/users/{{nickname}}"
}

View file

@ -0,0 +1,27 @@
{
"aliases": [
"https://{{subdomain}}/users/{{nickname}}"
],
"links": [
{
"href": "https://{{subdomain}}/users/{{nickname}}",
"rel": "http://webfinger.net/rel/profile-page",
"type": "text/html"
},
{
"href": "https://{{subdomain}}/users/{{nickname}}",
"rel": "self",
"type": "application/activity+json"
},
{
"href": "https://{{subdomain}}/users/{{nickname}}",
"rel": "self",
"type": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
},
{
"rel": "http://ostatus.org/schema/1.0/subscribe",
"template": "https://{{subdomain}}/ostatus_subscribe?acct={uri}"
}
],
"subject": "acct:{{nickname}}@{{domain}}"
}

View file

@ -738,6 +738,116 @@ test "gets an existing user by ap_id" do
end end
end end
describe "get_or_fetch/1 remote users with tld, while BE is runned on subdomain" do
setup do: clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true)
test "for mastodon" do
Tesla.Mock.mock(fn
%{url: "https://example.com/.well-known/host-meta"} ->
%Tesla.Env{
status: 302,
headers: [{"location", "https://sub.example.com/.well-known/host-meta"}]
}
%{url: "https://sub.example.com/.well-known/host-meta"} ->
%Tesla.Env{
status: 200,
body:
"test/fixtures/webfinger/masto-host-meta.xml"
|> File.read!()
|> String.replace("{{domain}}", "sub.example.com")
}
%{url: "https://sub.example.com/.well-known/webfinger?resource=acct:a@example.com"} ->
%Tesla.Env{
status: 200,
body:
"test/fixtures/webfinger/masto-webfinger.json"
|> File.read!()
|> String.replace("{{nickname}}", "a")
|> String.replace("{{domain}}", "example.com")
|> String.replace("{{subdomain}}", "sub.example.com"),
headers: [{"content-type", "application/jrd+json"}]
}
%{url: "https://sub.example.com/users/a"} ->
%Tesla.Env{
status: 200,
body:
"test/fixtures/webfinger/masto-user.json"
|> File.read!()
|> String.replace("{{nickname}}", "a")
|> String.replace("{{domain}}", "sub.example.com"),
headers: [{"content-type", "application/activity+json"}]
}
%{url: "https://sub.example.com/users/a/collections/featured"} ->
%Tesla.Env{
status: 200,
body:
File.read!("test/fixtures/users_mock/masto_featured.json")
|> String.replace("{{domain}}", "sub.example.com")
|> String.replace("{{nickname}}", "a"),
headers: [{"content-type", "application/activity+json"}]
}
end)
ap_id = "a@example.com"
{:ok, fetched_user} = User.get_or_fetch(ap_id)
assert fetched_user.ap_id == "https://sub.example.com/users/a"
assert fetched_user.nickname == "a@example.com"
end
test "for pleroma" do
Tesla.Mock.mock(fn
%{url: "https://example.com/.well-known/host-meta"} ->
%Tesla.Env{
status: 302,
headers: [{"location", "https://sub.example.com/.well-known/host-meta"}]
}
%{url: "https://sub.example.com/.well-known/host-meta"} ->
%Tesla.Env{
status: 200,
body:
"test/fixtures/webfinger/pleroma-host-meta.xml"
|> File.read!()
|> String.replace("{{domain}}", "sub.example.com")
}
%{url: "https://sub.example.com/.well-known/webfinger?resource=acct:a@example.com"} ->
%Tesla.Env{
status: 200,
body:
"test/fixtures/webfinger/pleroma-webfinger.json"
|> File.read!()
|> String.replace("{{nickname}}", "a")
|> String.replace("{{domain}}", "example.com")
|> String.replace("{{subdomain}}", "sub.example.com"),
headers: [{"content-type", "application/jrd+json"}]
}
%{url: "https://sub.example.com/users/a"} ->
%Tesla.Env{
status: 200,
body:
"test/fixtures/webfinger/pleroma-user.json"
|> File.read!()
|> String.replace("{{nickname}}", "a")
|> String.replace("{{domain}}", "sub.example.com"),
headers: [{"content-type", "application/activity+json"}]
}
end)
ap_id = "a@example.com"
{:ok, fetched_user} = User.get_or_fetch(ap_id)
assert fetched_user.ap_id == "https://sub.example.com/users/a"
assert fetched_user.nickname == "a@example.com"
end
end
describe "fetching a user from nickname or trying to build one" do describe "fetching a user from nickname or trying to build one" do
test "gets an existing user" do test "gets an existing user" do
user = insert(:user) user = insert(:user)

View file

@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do
use Pleroma.Web.ConnCase use Pleroma.Web.ConnCase, async: true
alias Pleroma.MFA alias Pleroma.MFA
alias Pleroma.MFA.TOTP alias Pleroma.MFA.TOTP

View file

@ -48,6 +48,35 @@ test "Webfinger JRD" do
] ]
end end
test "reach user on tld, while pleroma is runned on subdomain" do
Pleroma.Web.Endpoint.config_change(
[{Pleroma.Web.Endpoint, url: [host: "sub.example.com"]}],
[]
)
clear_config([Pleroma.Web.Endpoint, :url, :host], "sub.example.com")
clear_config([Pleroma.Web.WebFinger, :domain], "example.com")
user = insert(:user, ap_id: "https://sub.example.com/users/bobby", nickname: "bobby")
response =
build_conn()
|> put_req_header("accept", "application/jrd+json")
|> get("/.well-known/webfinger?resource=acct:#{user.nickname}@example.com")
|> json_response(200)
assert response["subject"] == "acct:#{user.nickname}@example.com"
assert response["aliases"] == ["https://sub.example.com/users/#{user.nickname}"]
on_exit(fn ->
Pleroma.Web.Endpoint.config_change(
[{Pleroma.Web.Endpoint, url: [host: "localhost"]}],
[]
)
end)
end
test "it returns 404 when user isn't found (JSON)" do test "it returns 404 when user isn't found (JSON)" do
result = result =
build_conn() build_conn()

View file

@ -47,7 +47,7 @@ test "returns error for nonsensical input" do
test "returns error when there is no content-type header" do test "returns error when there is no content-type header" do
Tesla.Mock.mock(fn Tesla.Mock.mock(fn
%{url: "http://social.heldscal.la/.well-known/host-meta"} -> %{url: "https://social.heldscal.la/.well-known/host-meta"} ->
{:ok, {:ok,
%Tesla.Env{ %Tesla.Env{
status: 200, status: 200,
@ -120,7 +120,7 @@ test "it gets the xrd endpoint for hubzilla" do
test "it gets the xrd endpoint for statusnet" do test "it gets the xrd endpoint for statusnet" do
{:ok, template} = WebFinger.find_lrdd_template("status.alpicola.com") {:ok, template} = WebFinger.find_lrdd_template("status.alpicola.com")
assert template == "http://status.alpicola.com/main/xrd?uri={uri}" assert template == "https://status.alpicola.com/main/xrd?uri={uri}"
end end
test "it works with idna domains as nickname" do test "it works with idna domains as nickname" do
@ -147,7 +147,7 @@ test "respects json content-type" do
headers: [{"content-type", "application/jrd+json"}] headers: [{"content-type", "application/jrd+json"}]
}} }}
%{url: "http://mastodon.social/.well-known/host-meta"} -> %{url: "https://mastodon.social/.well-known/host-meta"} ->
{:ok, {:ok,
%Tesla.Env{ %Tesla.Env{
status: 200, status: 200,
@ -170,7 +170,7 @@ test "respects xml content-type" do
headers: [{"content-type", "application/xrd+xml"}] headers: [{"content-type", "application/xrd+xml"}]
}} }}
%{url: "http://pawoo.net/.well-known/host-meta"} -> %{url: "https://pawoo.net/.well-known/host-meta"} ->
{:ok, {:ok,
%Tesla.Env{ %Tesla.Env{
status: 200, status: 200,

View file

@ -424,14 +424,6 @@ def get("http://mastodon.example.org/users/gargron", _, _, [
{:error, :nxdomain} {:error, :nxdomain}
end end
def get("http://osada.macgirvin.com/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 404,
body: ""
}}
end
def get("https://osada.macgirvin.com/.well-known/host-meta", _, _, _) do def get("https://osada.macgirvin.com/.well-known/host-meta", _, _, _) do
{:ok, {:ok,
%Tesla.Env{ %Tesla.Env{
@ -765,7 +757,7 @@ def get(
{:ok, %Tesla.Env{status: 406, body: ""}} {:ok, %Tesla.Env{status: 406, body: ""}}
end end
def get("http://squeet.me/.well-known/host-meta", _, _, _) do def get("https://squeet.me/.well-known/host-meta", _, _, _) do
{:ok, {:ok,
%Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/squeet.me_host_meta")}} %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/squeet.me_host_meta")}}
end end
@ -806,7 +798,7 @@ def get(
{:ok, %Tesla.Env{status: 200, body: "", headers: [{"content-type", "application/jrd+json"}]}} {:ok, %Tesla.Env{status: 200, body: "", headers: [{"content-type", "application/jrd+json"}]}}
end end
def get("http://framatube.org/.well-known/host-meta", _, _, _) do def get("https://framatube.org/.well-known/host-meta", _, _, _) do
{:ok, {:ok,
%Tesla.Env{ %Tesla.Env{
status: 200, status: 200,
@ -815,7 +807,7 @@ def get("http://framatube.org/.well-known/host-meta", _, _, _) do
end end
def get( def get(
"http://framatube.org/main/xrd?uri=acct:framasoft@framatube.org", "https://framatube.org/main/xrd?uri=acct:framasoft@framatube.org",
_, _,
_, _,
[{"accept", "application/xrd+xml,application/jrd+json"}] [{"accept", "application/xrd+xml,application/jrd+json"}]
@ -850,7 +842,7 @@ def get(
}} }}
end end
def get("http://status.alpicola.com/.well-known/host-meta", _, _, _) do def get("https://status.alpicola.com/.well-known/host-meta", _, _, _) do
{:ok, {:ok,
%Tesla.Env{ %Tesla.Env{
status: 200, status: 200,
@ -858,7 +850,7 @@ def get("http://status.alpicola.com/.well-known/host-meta", _, _, _) do
}} }}
end end
def get("http://macgirvin.com/.well-known/host-meta", _, _, _) do def get("https://macgirvin.com/.well-known/host-meta", _, _, _) do
{:ok, {:ok,
%Tesla.Env{ %Tesla.Env{
status: 200, status: 200,
@ -866,7 +858,7 @@ def get("http://macgirvin.com/.well-known/host-meta", _, _, _) do
}} }}
end end
def get("http://gerzilla.de/.well-known/host-meta", _, _, _) do def get("https://gerzilla.de/.well-known/host-meta", _, _, _) do
{:ok, {:ok,
%Tesla.Env{ %Tesla.Env{
status: 200, status: 200,