2023-12-14 16:31:58 +01:00
|
|
|
defmodule Mobilizon.Federation.NodeInfo do
|
|
|
|
@moduledoc """
|
|
|
|
Performs NodeInfo requests
|
|
|
|
"""
|
|
|
|
|
|
|
|
alias Mobilizon.Service.HTTP.WebfingerClient
|
|
|
|
require Logger
|
2023-12-22 16:03:59 +01:00
|
|
|
import Mobilizon.Service.HTTP.Utils, only: [is_content_type?: 2]
|
2023-12-14 16:31:58 +01:00
|
|
|
|
|
|
|
@application_uri "https://www.w3.org/ns/activitystreams#Application"
|
2023-12-20 17:52:27 +01:00
|
|
|
@nodeinfo_rel_2_0 "http://nodeinfo.diaspora.software/ns/schema/2.0"
|
|
|
|
@nodeinfo_rel_2_1 "http://nodeinfo.diaspora.software/ns/schema/2.1"
|
|
|
|
|
2023-12-14 16:31:58 +01:00
|
|
|
@env Application.compile_env(:mobilizon, :env)
|
|
|
|
|
|
|
|
@spec application_actor(String.t()) :: String.t() | nil
|
|
|
|
def application_actor(host) do
|
2023-12-20 17:52:27 +01:00
|
|
|
Logger.debug("Fetching application actor from NodeInfo data for domain #{host}")
|
2023-12-14 16:31:58 +01:00
|
|
|
|
2023-12-20 17:52:27 +01:00
|
|
|
case fetch_nodeinfo_endpoint(host) do
|
|
|
|
{:ok, body} ->
|
2023-12-14 16:31:58 +01:00
|
|
|
extract_application_actor(body)
|
|
|
|
|
2023-12-22 18:19:04 +01:00
|
|
|
{:error, _err} ->
|
2023-12-14 16:31:58 +01:00
|
|
|
nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-12-20 17:52:27 +01:00
|
|
|
@spec nodeinfo(String.t()) :: {:ok, map()} | {:error, atom()}
|
|
|
|
def nodeinfo(host) do
|
|
|
|
Logger.debug("Fetching NodeInfo details for domain #{host}")
|
|
|
|
|
|
|
|
with {:ok, endpoint} when is_binary(endpoint) <- fetch_nodeinfo_details(host),
|
|
|
|
:ok <- Logger.debug("Going to get NodeInfo information from URL #{endpoint}"),
|
2023-12-22 16:03:59 +01:00
|
|
|
{:ok, %{body: body, status: code, headers: headers}} when code in 200..299 <-
|
|
|
|
WebfingerClient.get(endpoint),
|
|
|
|
{:ok, body} <- validate_json_response(body, headers) do
|
2023-12-20 17:52:27 +01:00
|
|
|
Logger.debug("Found nodeinfo information for domain #{host}")
|
|
|
|
{:ok, body}
|
|
|
|
else
|
|
|
|
{:error, err} ->
|
|
|
|
{:error, err}
|
|
|
|
|
|
|
|
err ->
|
|
|
|
Logger.debug("Failed to fetch NodeInfo data from endpoint #{inspect(err)}")
|
|
|
|
{:error, :node_info_endpoint_http_error}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-12-14 16:31:58 +01:00
|
|
|
defp extract_application_actor(body) do
|
|
|
|
body
|
2023-12-19 09:52:32 +01:00
|
|
|
|> Map.get("links", [])
|
|
|
|
|> Enum.find(%{"rel" => @application_uri, "href" => nil}, fn %{"rel" => rel, "href" => href} ->
|
2023-12-14 16:31:58 +01:00
|
|
|
rel == @application_uri and is_binary(href)
|
|
|
|
end)
|
2023-12-19 09:52:32 +01:00
|
|
|
|> Map.get("href")
|
2023-12-14 16:31:58 +01:00
|
|
|
end
|
2023-12-20 17:52:27 +01:00
|
|
|
|
|
|
|
@spec fetch_nodeinfo_endpoint(String.t()) :: {:ok, map()} | {:error, atom()}
|
|
|
|
defp fetch_nodeinfo_endpoint(host) do
|
|
|
|
prefix = if @env !== :dev, do: "https", else: "http"
|
|
|
|
|
|
|
|
case WebfingerClient.get("#{prefix}://#{host}/.well-known/nodeinfo") do
|
2023-12-22 16:03:59 +01:00
|
|
|
{:ok, %{body: body, status: code, headers: headers}} when code in 200..299 ->
|
|
|
|
validate_json_response(body, headers)
|
2023-12-20 17:52:27 +01:00
|
|
|
|
|
|
|
err ->
|
|
|
|
Logger.debug("Failed to fetch NodeInfo data #{inspect(err)}")
|
|
|
|
{:error, :node_info_meta_http_error}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
@spec fetch_nodeinfo_details(String.t()) :: {:ok, String.t()} | {:error, atom()}
|
|
|
|
defp fetch_nodeinfo_details(host) do
|
|
|
|
with {:ok, body} <- fetch_nodeinfo_endpoint(host) do
|
|
|
|
extract_nodeinfo_endpoint(body)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
@spec extract_nodeinfo_endpoint(map()) ::
|
|
|
|
{:ok, String.t()}
|
|
|
|
| {:error, :no_node_info_endpoint_found | :no_valid_node_info_endpoint_found}
|
|
|
|
defp extract_nodeinfo_endpoint(body) do
|
|
|
|
links = Map.get(body, "links", [])
|
|
|
|
|
|
|
|
relation =
|
|
|
|
find_nodeinfo_relation(links, @nodeinfo_rel_2_1) ||
|
|
|
|
find_nodeinfo_relation(links, @nodeinfo_rel_2_0)
|
|
|
|
|
|
|
|
if is_nil(relation) do
|
|
|
|
{:error, :no_node_info_endpoint_found}
|
|
|
|
else
|
|
|
|
endpoint = Map.get(relation, "href")
|
|
|
|
|
|
|
|
if is_nil(endpoint) do
|
|
|
|
{:error, :no_valid_node_info_endpoint_found}
|
|
|
|
else
|
|
|
|
{:ok, endpoint}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp find_nodeinfo_relation(links, relation) do
|
|
|
|
Enum.find(links, fn %{"rel" => rel, "href" => href} ->
|
|
|
|
rel == relation and is_binary(href)
|
|
|
|
end)
|
|
|
|
end
|
2023-12-22 16:03:59 +01:00
|
|
|
|
|
|
|
@spec validate_json_response(map() | String.t(), list()) ::
|
|
|
|
{:ok, String.t()} | {:error, :bad_content_type | :body_not_json}
|
|
|
|
defp validate_json_response(body, headers) do
|
|
|
|
cond do
|
|
|
|
!is_content_type?(headers, "application/json") ->
|
|
|
|
{:error, :bad_content_type}
|
|
|
|
|
|
|
|
!is_map(body) ->
|
|
|
|
{:error, :body_not_json}
|
|
|
|
|
|
|
|
true ->
|
|
|
|
{:ok, body}
|
|
|
|
end
|
|
|
|
end
|
2023-12-14 16:31:58 +01:00
|
|
|
end
|