Merge branch 'multitenancy' into 'develop'

Allow using multiple domains for WebFinger

See merge request pleroma/pleroma!3965
This commit is contained in:
mkljczk 2025-04-11 12:34:12 +00:00
commit 1325c34c9b
44 changed files with 1247 additions and 40 deletions

View file

@ -0,0 +1 @@
Allow using multiple domains for WebFinger

View file

@ -261,7 +261,10 @@ config :pleroma, :instance,
max_endorsed_users: 20,
birthday_required: false,
birthday_min_age: 0,
max_media_attachments: 1_000
max_media_attachments: 1_000,
multitenancy: %{
enabled: false
}
config :pleroma, :welcome,
direct_message: [
@ -601,6 +604,7 @@ config :pleroma, Oban,
],
plugins: [{Oban.Plugins.Pruner, max_age: 900}],
crontab: [
{"0 0 * * 0", Pleroma.Workers.Cron.CheckDomainsResolveWorker},
{"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker},
{"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker},
{"*/10 * * * *", Pleroma.Workers.Cron.AppCleanupWorker}

View file

@ -1131,6 +1131,23 @@ config :pleroma, :config_description, [
suggestions: [
"en"
]
},
%{
key: :multitenancy,
type: :map,
description: "Multitenancy support",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables allowing multiple Webfinger domains"
},
%{
key: :separate_timelines,
type: :boolean,
description: "Only display posts from own domain on local timeline"
}
]
}
]
},

View file

@ -0,0 +1,30 @@
# How to serve multiple domains for Pleroma user identifiers
It is possible to use multiple domains for WebFinger identifiers. If configured, users can select from the available domains during registration. Domains can be set by instance administrator and can be marked as either public (everyone can choose it) or private (only available when admin creates a user)
## Configuring
### Configuring Pleroma
To enable using multiple domains, append the following to your `prod.secret.exs` or `dev.secret.exs`:
```elixir
config :pleroma, :instance, :multitenancy, enabled: true
```
Creating, updating and deleting domains is available from the admin API.
### Configuring WebFinger domains
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 the additional domains is to redirect `/.well-known/host-meta` to the domain used by Pleroma.
With nginx, it would be as simple as adding:
```nginx
location = /.well-known/host-meta {
return 301 https://pleroma.example.org$request_uri;
}
```
in the additional domain's server block.

View file

@ -1755,6 +1755,73 @@ Note that this differs from the Mastodon API variant: Mastodon API only returns
{}
```
## `GET /api/v1/pleroma/admin/domains`
### List of domains
- Response: JSON, list of domains
```json
[
{
"id": "1",
"domain": "example.org",
"public": false,
"resolves": true,
"last_checked_at": "2023-11-17T12:13:05"
}
]
```
## `POST /api/v1/pleroma/admin/domains`
### Create a domain
- Params:
- `domain`: string, required, domain name
- `public`: boolean, optional, defaults to false, whether it is possible to register an account under the domain by everyone
- Response: JSON, created announcement
```json
{
"id": "1",
"domain": "example.org",
"public": true,
"resolves": false,
"last_checked_at": null
}
```
## `POST /api/v1/pleroma/admin/domains/:id`
### Change domain publicity
- Params:
- `public`: boolean, whether it is possible to register an account under the domain by everyone
- Response: JSON, updated domain
```json
{
"id": "1",
"domain": "example.org",
"public": false,
"resolves": true,
"last_checked_at": "2023-11-17T12:13:05"
}
```
## `DELETE /api/v1/pleroma/admin/domains/:id`
### Delete a domain
- Response: JSON, empty object
```json
{}
```
## `GET /api/v1/pleroma/admin/rules`

View file

@ -312,6 +312,7 @@ Has these additional parameters (which are the same as in Pleroma-API):
- `captcha_answer_data`: optional, contains provider-specific captcha data
- `token`: invite token required when the registrations aren't public.
- `language`: optional, user's preferred language for receiving emails (digest, confirmation, etc.), default to the language set in the `userLanguage` cookies or `Accept-Language` header.
- `domain`: optional, domain id, if multitenancy is enabled.
## Instance

View file

@ -173,7 +173,8 @@ defmodule Pleroma.Application do
),
build_cachex("rel_me", limit: 2500),
build_cachex("host_meta", default_ttl: :timer.minutes(120), limit: 5_000),
build_cachex("translations", default_ttl: :timer.hours(24), limit: 5_000)
build_cachex("translations", default_ttl: :timer.hours(24), limit: 5_000),
build_cachex("domain", limit: 2500)
]
end

81
lib/pleroma/domain.ex Normal file
View file

@ -0,0 +1,81 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Domain do
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias Pleroma.Repo
schema "domains" do
field(:domain, :string, default: "")
field(:public, :boolean, default: false)
field(:resolves, :boolean, default: false)
field(:last_checked_at, :naive_datetime)
timestamps(type: :utc_datetime)
end
def changeset(%__MODULE__{} = domain, params \\ %{}) do
domain
|> cast(params, [:domain, :public])
|> validate_required([:domain])
|> update_change(:domain, &String.downcase/1)
|> unique_constraint(:domain)
end
def update_changeset(%__MODULE__{} = domain, params \\ %{}) do
domain
|> cast(params, [:public])
end
def update_state_changeset(%__MODULE__{} = domain, resolves) do
domain
|> cast(
%{
resolves: resolves,
last_checked_at: NaiveDateTime.utc_now()
},
[:resolves, :last_checked_at]
)
end
def list do
__MODULE__
|> order_by(asc: :id)
|> Repo.all()
end
def get(id), do: Repo.get(__MODULE__, id)
def get_by_name(domain) do
__MODULE__
|> where(domain: ^domain)
|> Repo.one()
end
def create(params) do
%__MODULE__{}
|> changeset(params)
|> Repo.insert()
end
def update(params, id) do
get(id)
|> update_changeset(params)
|> Repo.update()
end
def delete(id) do
get(id)
|> Repo.delete()
end
def cached_list do
@cachex.fetch!(:domain_cache, "domains_list", fn _ -> list() end)
end
end

View file

@ -15,6 +15,7 @@ defmodule Pleroma.User do
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Delivery
alias Pleroma.Domain
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Emoji
alias Pleroma.FollowingRelationship
@ -163,6 +164,8 @@ defmodule Pleroma.User do
field(:show_birthday, :boolean, default: false)
field(:language, :string)
belongs_to(:domain, Domain)
embeds_one(
:notification_settings,
Pleroma.User.NotificationSetting,
@ -860,16 +863,18 @@ defmodule Pleroma.User do
:accepts_chat_messages,
:registration_reason,
:birthday,
:language
:language,
:domain_id
])
|> validate_required([:name, :nickname, :password, :password_confirmation])
|> validate_confirmation(:password)
|> unique_constraint(:email)
|> validate_format(:email, @email_regex)
|> validate_email_not_in_blacklisted_domain(:email)
|> unique_constraint(:nickname)
|> validate_not_restricted_nickname(:nickname)
|> validate_format(:nickname, local_nickname_regex())
|> fix_nickname(Map.get(params, :domain_id), opts[:from_admin])
|> validate_not_restricted_nickname(:nickname)
|> unique_constraint(:nickname)
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit)
|> validate_length(:registration_reason, max: reason_limit)
@ -883,6 +888,26 @@ defmodule Pleroma.User do
|> put_private_key()
end
defp fix_nickname(changeset, domain_id, from_admin) when not is_nil(domain_id) do
with {:domain, domain} <- {:domain, Domain.get(domain_id)},
{:domain_allowed, true} <- {:domain_allowed, from_admin || domain.public} do
nickname = get_field(changeset, :nickname)
changeset
|> put_change(:nickname, nickname <> "@" <> domain.domain)
|> put_change(:domain, domain)
else
{:domain_allowed, false} ->
changeset
|> add_error(:domain, "not allowed to use this domain")
_ ->
changeset
end
end
defp fix_nickname(changeset, _, _), do: changeset
def validate_not_restricted_nickname(changeset, field) do
validate_change(changeset, field, fn _, value ->
valid? =
@ -943,7 +968,9 @@ defmodule Pleroma.User do
end
defp put_ap_id(changeset) do
ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
nickname = get_field(changeset, :nickname)
ap_id = ap_id(%User{nickname: nickname})
put_change(changeset, :ap_id, ap_id)
end
@ -1356,6 +1383,13 @@ defmodule Pleroma.User do
restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
get_cached_by_nickname(nickname_or_id)
String.contains?(nickname_or_id, "@") ->
with %User{local: true} = user <- get_cached_by_nickname(nickname_or_id) do
user
else
_ -> nil
end
true ->
nil
end

View file

@ -955,6 +955,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_local(query, _), do: query
defp restrict_domain(query, %{domain_id: 0}) do
query
|> join(:inner, [activity], u in User,
as: :domain_user,
on: activity.actor == u.ap_id and is_nil(u.domain_id)
)
end
defp restrict_domain(query, %{domain_id: domain_id}) do
query
|> join(:inner, [activity], u in User,
as: :domain_user,
on: activity.actor == u.ap_id and u.domain_id == ^domain_id
)
end
defp restrict_domain(query, _), do: query
defp restrict_remote(query, %{remote: true}) do
from(activity in query, where: activity.local == false)
end
@ -1443,6 +1461,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_replies(opts)
|> restrict_since(opts)
|> restrict_local(opts)
|> restrict_domain(opts)
|> restrict_remote(opts)
|> restrict_actor(opts)
|> restrict_type(opts)

View file

@ -52,6 +52,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
when action in [:activity, :object]
)
plug(
Pleroma.Web.Plugs.SetDomainPlug
when action in [:following, :followers, :pinned, :inbox, :outbox, :update_outbox]
)
plug(
Pleroma.Web.Plugs.SetNicknameWithDomainPlug
when action in [:following, :followers, :pinned, :inbox, :outbox, :update_outbox]
)
plug(:log_inbox_metadata when action in [:inbox])
plug(:set_requester_reachable when action in [:inbox])
plug(:relay_active? when action in [:relay])
@ -540,7 +550,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
with {:ok, object} <-
ActivityPub.upload(
file,
actor: User.ap_id(user),
actor: user.ap_id,
description: Map.get(data, "description")
) do
Logger.debug(inspect(object))

View file

@ -0,0 +1,78 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.DomainController do
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
use Pleroma.Web, :controller
alias Pleroma.Domain
alias Pleroma.Web.AdminAPI
alias Pleroma.Web.Plugs.OAuthScopesPlug
import Pleroma.Web.ControllerHelper,
only: [
json_response: 3
]
plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(
OAuthScopesPlug,
%{scopes: ["admin:write"]}
when action in [:create, :update, :delete]
)
plug(OAuthScopesPlug, %{scopes: ["admin:read"]} when action == :index)
action_fallback(AdminAPI.FallbackController)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.DomainOperation
def index(conn, _) do
domains = Domain.list()
render(conn, "index.json", domains: domains)
end
def create(%{body_params: params} = conn, _) do
with {:domain_not_used, true} <-
{:domain_not_used, params[:domain] !== Pleroma.Web.WebFinger.host()},
{:ok, domain} <- Domain.create(params),
_ <- @cachex.del(:domain_cache, "domains_list") do
Pleroma.Workers.CheckDomainResolveWorker.enqueue("check_domain_resolve", %{
"id" => domain.id
})
render(conn, "show.json", domain: domain)
else
{:domain_not_used, false} ->
{:error, :invalid_domain}
{:error, changeset} ->
errors = Map.new(changeset.errors, fn {key, {error, _}} -> {key, error} end)
{:errors, errors}
end
end
def update(%{body_params: params} = conn, %{id: id}) do
{:ok, domain} =
params
|> Domain.update(id)
@cachex.del(:domain_cache, "domains_list")
render(conn, "show.json", domain: domain)
end
def delete(conn, %{id: id}) do
with {:ok, _} <- Domain.delete(id),
_ <- @cachex.del(:domain_cache, :domains_list) do
json(conn, %{})
else
_ -> json_response(conn, :bad_request, "")
end
end
end

View file

@ -8,6 +8,7 @@ defmodule Pleroma.Web.AdminAPI.UserController do
import Pleroma.Web.ControllerHelper,
only: [fetch_integer_param: 3]
alias Pleroma.Domain
alias Pleroma.ModerationLog
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Builder
@ -150,17 +151,25 @@ defmodule Pleroma.Web.AdminAPI.UserController do
) do
changesets =
users
|> Enum.map(fn %{nickname: nickname, email: email, password: password} ->
|> Enum.map(fn %{nickname: nickname, email: email, password: password} = user ->
domain_id = Map.get(user, :domain)
domain =
if domain_id do
Domain.get(domain_id)
end
user_data = %{
nickname: nickname,
name: nickname,
email: email,
password: password,
password_confirmation: password,
bio: "."
bio: ".",
domain: domain
}
User.register_changeset(%User{}, user_data, need_confirmation: false)
User.register_changeset(%User{}, user_data, need_confirmation: false, from_admin: true)
end)
|> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)

View file

@ -0,0 +1,35 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.DomainView do
use Pleroma.Web, :view
alias Pleroma.Domain
alias Pleroma.Web.CommonAPI.Utils
def render("index.json", %{domains: domains} = assigns) do
render_many(domains, __MODULE__, "show.json", assigns |> Map.delete("domains"))
end
def render("show.json", %{domain: %Domain{} = domain} = assigns) do
%{
id: domain.id |> to_string(),
domain: domain.domain,
public: domain.public
}
|> maybe_put_resolve_information(domain, assigns)
end
defp maybe_put_resolve_information(map, _domain, %{admin: false}) do
map
end
defp maybe_put_resolve_information(map, domain, _assigns) do
map
|> Map.merge(%{
resolves: domain.resolves,
last_checked_at: Utils.to_masto_date(domain.last_checked_at)
})
end
end

View file

@ -105,7 +105,8 @@ defmodule Pleroma.Web.ApiSpec do
"Report management",
"Status administration",
"User administration",
"Announcement management"
"Announcement management",
"Domain managment"
]
},
%{"name" => "Applications", "tags" => ["Applications", "Push subscriptions"]},

View file

@ -617,7 +617,8 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
type: :string,
nullable: true,
description: "User's preferred language for emails"
}
},
domain: %Schema{type: :string, nullable: true}
},
example: %{
"username" => "cofe",

View file

@ -0,0 +1,113 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Admin.DomainOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.ApiError
import Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def index_operation do
%Operation{
tags: ["Domain managment"],
summary: "Retrieve list of domains",
operationId: "AdminAPI.DomainController.index",
security: [%{"oAuth" => ["admin:read"]}],
responses: %{
200 =>
Operation.response("Response", "application/json", %Schema{
type: :array,
items: domain()
}),
403 => Operation.response("Forbidden", "application/json", ApiError)
}
}
end
def create_operation do
%Operation{
tags: ["Domain managment"],
summary: "Create new domain",
operationId: "AdminAPI.DomainController.create",
security: [%{"oAuth" => ["admin:write"]}],
parameters: admin_api_params(),
requestBody: request_body("Parameters", create_request(), required: true),
responses: %{
200 => Operation.response("Response", "application/json", domain()),
400 => Operation.response("Bad Request", "application/json", ApiError),
403 => Operation.response("Forbidden", "application/json", ApiError)
}
}
end
def update_operation do
%Operation{
tags: ["Domain managment"],
summary: "Modify existing domain",
operationId: "AdminAPI.DomainController.update",
security: [%{"oAuth" => ["admin:write"]}],
parameters: [Operation.parameter(:id, :path, :string, "Domain ID")],
requestBody: request_body("Parameters", update_request(), required: true),
responses: %{
200 => Operation.response("Response", "application/json", domain()),
400 => Operation.response("Bad Request", "application/json", ApiError),
403 => Operation.response("Forbidden", "application/json", ApiError)
}
}
end
def delete_operation do
%Operation{
tags: ["Domain managment"],
summary: "Delete domain",
operationId: "AdminAPI.DomainController.delete",
parameters: [Operation.parameter(:id, :path, :string, "Domain ID")],
security: [%{"oAuth" => ["admin:write"]}],
responses: %{
200 => empty_object_response(),
404 => Operation.response("Not Found", "application/json", ApiError),
403 => Operation.response("Forbidden", "application/json", ApiError)
}
}
end
defp create_request do
%Schema{
type: :object,
required: [:domain],
properties: %{
domain: %Schema{type: :string},
public: %Schema{type: :boolean, nullable: true}
}
}
end
defp update_request do
%Schema{
type: :object,
properties: %{
public: %Schema{type: :boolean, nullable: true}
}
}
end
defp domain do
%Schema{
type: :object,
properties: %{
id: %Schema{type: :integer},
domain: %Schema{type: :string},
public: %Schema{type: :boolean},
resolves: %Schema{type: :boolean},
last_checked_at: %Schema{type: :string, format: "date-time", nullable: true}
}
}
end
end

View file

@ -82,7 +82,8 @@ defmodule Pleroma.Web.ApiSpec.Admin.UserOperation do
properties: %{
nickname: %Schema{type: :string},
email: %Schema{type: :string},
password: %Schema{type: :string}
password: %Schema{type: :string},
domain: %Schema{type: :string, nullable: true}
}
}
}

View file

@ -134,7 +134,10 @@ defmodule Pleroma.Web.Endpoint do
plug(Phoenix.CodeReloader)
end
plug(Pleroma.Web.Plugs.TrailingFormatPlug)
plug(Pleroma.Web.Plugs.TrailingFormatPlug,
supported_formats: ["html", "xml", "rss", "atom", "activity+json", "json"]
)
plug(Plug.RequestId)
plug(Plug.Logger, log: :debug)

View file

@ -12,6 +12,8 @@ defmodule Pleroma.Web.Feed.UserController do
alias Pleroma.Web.Feed.FeedView
plug(Pleroma.Web.Plugs.SetFormatPlug when action in [:feed_redirect])
plug(Pleroma.Web.Plugs.SetDomainPlug when action in [:feed_redirect, :feed])
plug(Pleroma.Web.Plugs.SetNicknameWithDomainPlug when action in [:feed_redirect, :feed])
action_fallback(:errors)

View file

@ -6,7 +6,6 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
use Pleroma.Web, :controller
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.Plugs.OAuthScopesPlug
@ -28,7 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
with {:ok, object} <-
ActivityPub.upload(
file,
actor: User.ap_id(user),
actor: user.ap_id,
description: Map.get(data, :description)
) do
attachment_data = Map.put(object.data, "id", object.id)
@ -48,7 +47,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
with {:ok, object} <-
ActivityPub.upload(
file,
actor: User.ap_id(user),
actor: user.ap_id,
description: Map.get(data, :description)
) do
attachment_data = Map.put(object.data, "id", object.id)

View file

@ -36,6 +36,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
when action in [:public, :hashtag]
)
plug(Pleroma.Web.Plugs.SetDomainPlug when action in [:public, :hashtag])
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TimelineOperation
# GET /api/v1/timelines/home
@ -120,6 +122,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> Map.put(:instance, params[:instance])
# Restricts unfederated content to authenticated users
|> Map.put(:includes_local_public, not is_nil(user))
|> maybe_put_domain_id(conn)
|> ActivityPub.fetch_public_activities()
conn
@ -137,7 +140,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
render_error(conn, :unauthorized, "authorization required for timeline view")
end
defp hashtag_fetching(params, user, local_only) do
defp hashtag_fetching(conn, params, user, local_only) do
# Note: not sanitizing tag options at this stage (may be mix-cased, have duplicates etc.)
tags_any =
[params[:tag], params[:any]]
@ -156,6 +159,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> Map.put(:tag, tags_any)
|> Map.put(:tag_all, tag_all)
|> Map.put(:tag_reject, tag_reject)
|> maybe_put_domain_id(conn)
|> ActivityPub.fetch_public_activities()
end
@ -166,7 +170,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
if is_nil(user) and restrict_unauthenticated?(local_only) do
fail_on_bad_auth(conn)
else
activities = hashtag_fetching(params, user, local_only)
activities = hashtag_fetching(conn, params, user, local_only)
conn
|> add_link_headers(activities, %{"local" => local_only})
@ -189,6 +193,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> Map.put(:user, user)
|> Map.put(:muting_user, user)
|> Map.put(:local_only, params[:local])
|> maybe_put_domain_id(conn)
# we must filter the following list for the user to avoid leaking statuses the user
# does not actually have permission to see (for more info, peruse security issue #270).
@ -213,4 +218,19 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
_e -> render_error(conn, :forbidden, "Error.")
end
end
defp maybe_put_domain_id(%{local_only: true} = params, conn) do
separate_timelines = Config.get([:instance, :multitenancy, :separate_timelines])
if separate_timelines do
domain = Map.get(conn, :domain, %{id: 0})
params
|> Map.put(:domain_id, domain.id)
else
params
end
end
defp maybe_put_domain_id(params, _conn), do: params
end

View file

@ -325,7 +325,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
accepts_chat_messages: user.accepts_chat_messages,
favicon: favicon,
avatar_description: avatar_description,
header_description: header_description
header_description: header_description,
is_local: user.local
}
}
|> maybe_put_role(user, opts[:for])

View file

@ -6,7 +6,9 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
use Pleroma.Web, :view
alias Pleroma.Config
alias Pleroma.Domain
alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.AdminAPI.DomainView
@mastodon_api_level "2.7.2"
@ -157,7 +159,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
"pleroma:bookmark_folders",
if Pleroma.Language.LanguageDetector.configured?() do
"pleroma:language_detection"
end
end,
"pleroma:multitenancy"
]
|> Enum.filter(& &1)
end
@ -286,7 +289,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
birthday_required: Config.get([:instance, :birthday_required]),
birthday_min_age: Config.get([:instance, :birthday_min_age]),
translation: supported_languages(),
base_urls: base_urls
base_urls: base_urls,
multitenancy: multitenancy()
},
stats: %{mau: Pleroma.User.active_user_count()},
vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
@ -337,4 +341,21 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
target_languages: target_languages
}
end
defp multitenancy do
enabled = Config.get([:instance, :multitenancy, :enabled])
if enabled do
domains =
[%Domain{id: "", domain: Pleroma.Web.WebFinger.host(), public: true}] ++
Domain.cached_list()
%{
enabled: true,
domains: DomainView.render("index.json", domains: domains, admin: false)
}
else
nil
end
end
end

View file

@ -0,0 +1,28 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.SetDomainPlug do
use Pleroma.Web, :plug
alias Pleroma.Domain
def init(opts), do: opts
@impl true
def perform(%{host: domain} = conn, _opts) do
with true <- Pleroma.Config.get([:instance, :multitenancy, :enabled], false),
false <-
domain in [
Pleroma.Config.get([__MODULE__, :domain]),
Pleroma.Web.Endpoint.host()
],
%Domain{} = domain <- Domain.get_by_name(domain) do
Map.put(conn, :domain, domain)
else
_ -> conn
end
end
def perform(conn, _), do: conn
end

View file

@ -0,0 +1,24 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.SetNicknameWithDomainPlug do
use Pleroma.Web, :plug
def init(opts), do: opts
@impl true
def perform(%{domain: domain, params: params} = conn, opts) do
with key <- Keyword.get(opts, :key, "nickname"),
nickname <- Map.get(params, key),
false <- String.contains?(nickname, "@"),
nickname <- nickname <> "@" <> domain.domain,
params <- Map.put(params, "nickname", nickname) do
Map.put(conn, :params, params)
else
_ -> conn
end
end
def perform(conn, _), do: conn
end

View file

@ -1,9 +1,12 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.TrailingFormatPlug do
@moduledoc "Calls TrailingFormatPlug for specific paths. Ideally we would just do this in the router, but TrailingFormatPlug needs to be called before Plug.Parsers."
@moduledoc """
This plug is adapted from [`TrailingFormatPlug`](https://github.com/mschae/trailing_format_plug/blob/master/lib/trailing_format_plug.ex).
Ideally we would just do this in the router, but TrailingFormatPlug needs to be called before Plug.Parsers."
"""
@behaviour Plug
@paths [
@ -29,14 +32,53 @@ defmodule Pleroma.Web.Plugs.TrailingFormatPlug do
]
def init(opts) do
TrailingFormatPlug.init(opts)
opts
end
for path <- @paths do
def call(%{request_path: unquote(path) <> _} = conn, opts) do
TrailingFormatPlug.call(conn, opts)
path = conn.path_info |> List.last() |> String.split(".") |> Enum.reverse()
supported_formats = Keyword.get(opts, :supported_formats, nil)
case path do
[_] ->
conn
[format | fragments] ->
if supported_formats == nil || format in supported_formats do
new_path = fragments |> Enum.reverse() |> Enum.join(".")
path_fragments = List.replace_at(conn.path_info, -1, new_path)
params =
Plug.Conn.fetch_query_params(conn).params
|> update_params(new_path, format)
|> Map.put("_format", format)
%{
conn
| path_info: path_fragments,
query_params: params,
params: params
}
else
conn
end
end
end
end
def call(conn, _opts), do: conn
defp update_params(params, new_path, format) do
wildcard = Enum.find(params, fn {_, v} -> v == "#{new_path}.#{format}" end)
case wildcard do
{key, _} ->
Map.put(params, key, new_path)
_ ->
params
end
end
end

View file

@ -309,6 +309,11 @@ defmodule Pleroma.Web.Router do
post("/rules", RuleController, :create)
patch("/rules/:id", RuleController, :update)
delete("/rules/:id", RuleController, :delete)
get("/domains", DomainController, :index)
post("/domains", DomainController, :create)
patch("/domains/:id", DomainController, :update)
delete("/domains/:id", DomainController, :delete)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)

View file

@ -27,6 +27,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
:language,
Pleroma.Web.Gettext.normalize_locale(params[:language]) || fallback_language
)
|> maybe_put_domain_id(params[:domain])
if Pleroma.Config.get([:instance, :registrations_open]) do
create_user(params, opts)
@ -64,6 +65,14 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
end
end
defp maybe_put_domain_id(params, domain) do
if Pleroma.Config.get([:instance, :multitenancy, :enabled]) do
Map.put(params, :domain_id, domain)
else
params
end
end
def password_reset(nickname_or_email) do
with true <- is_binary(nickname_or_email),
%User{local: true, email: email, is_active: true} = user when is_binary(email) <-

View file

@ -33,15 +33,13 @@ defmodule Pleroma.Web.WebFinger do
def webfinger(resource, fmt) when fmt in ["XML", "JSON"] do
host = Pleroma.Web.Endpoint.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
regex = ~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@(?<domain>[a-z0-9A-Z_\.-]+)/
webfinger_domain = Pleroma.Config.get([__MODULE__, :domain])
with %{"username" => username} <- Regex.named_captures(regex, resource),
%User{} = user <- User.get_cached_by_nickname(username) do
with %{"username" => username, "domain" => domain} <- Regex.named_captures(regex, resource),
nickname <-
if(domain in [host, webfinger_domain], do: username, else: username <> "@" <> domain),
%User{local: true} = user <- User.get_cached_by_nickname(nickname) do
{:ok, represent_user(user, fmt)}
else
_e ->
@ -70,7 +68,7 @@ defmodule Pleroma.Web.WebFinger do
def represent_user(user, "JSON") do
%{
"subject" => "acct:#{user.nickname}@#{host()}",
"subject" => get_subject(user),
"aliases" => gather_aliases(user),
"links" => gather_links(user)
}
@ -90,12 +88,20 @@ defmodule Pleroma.Web.WebFinger do
:XRD,
%{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
[
{:Subject, "acct:#{user.nickname}@#{host()}"}
{:Subject, get_subject(user)}
] ++ aliases ++ links
}
|> XmlBuilder.to_doc()
end
defp get_subject(%User{nickname: nickname}) do
if String.contains?(nickname, "@") do
"acct:#{nickname}"
else
"acct:#{nickname}@#{host()}"
end
end
def host do
Pleroma.Config.get([__MODULE__, :domain]) || Pleroma.Web.Endpoint.host()
end

View file

@ -0,0 +1,36 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.CheckDomainResolveWorker do
use Oban.Worker, queue: :background
alias Pleroma.Domain
alias Pleroma.HTTP
alias Pleroma.Repo
alias Pleroma.Web.Endpoint
alias Pleroma.Web.WebFinger
@impl Oban.Worker
def perform(%Job{args: %{"id" => domain_id}}) do
domain = Domain.get(domain_id)
resolves =
with {:ok, %Tesla.Env{status: status, body: hostmeta_body}} when status in 200..299 <-
HTTP.get("https://" <> domain.domain <> "/.well-known/host-meta"),
{:ok, template} <- WebFinger.get_template_from_xml(hostmeta_body),
base_url <- Endpoint.url(),
true <- template == "#{base_url}/.well-known/webfinger?resource={uri}" do
true
else
_ -> false
end
domain
|> Domain.update_state_changeset(resolves)
|> Repo.update()
end
@impl Oban.Worker
def timeout(_job), do: :timer.seconds(5)
end

View file

@ -0,0 +1,34 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.Cron.CheckDomainsResolveWorker do
@moduledoc """
The worker to check if alternative domains resolve correctly.
"""
use Oban.Worker, queue: "check_domain_resolve"
alias Pleroma.Domain
alias Pleroma.Repo
import Ecto.Query
require Logger
@impl Oban.Worker
def perform(_job) do
domains =
Domain
|> select([d], d.id)
|> Repo.all()
Enum.each(domains, fn domain_id ->
Pleroma.Workers.CheckDomainResolveWorker.enqueue("check_domain_resolve", %{
"id" => domain_id
})
end)
:ok
end
end

View file

@ -148,7 +148,6 @@ defmodule Pleroma.Mixfile do
{:oban, "~> 2.18.0"},
{:gettext, "~> 0.20"},
{:bcrypt_elixir, "~> 2.2"},
{:trailing_format_plug, "~> 0.0.7"},
{:fast_sanitize, "~> 0.2.0"},
{:html_entities, "~> 0.5", override: true},
{:calendar, "~> 1.0"},

View file

@ -143,7 +143,6 @@
"thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"},
"timex": {:hex, :timex, "3.7.7", "3ed093cae596a410759104d878ad7b38e78b7c2151c6190340835515d4a46b8a", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "0ec4b09f25fe311321f9fc04144a7e3affe48eb29481d7a5583849b6c4dfa0a7"},
"toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"},
"tzdata": {:hex, :tzdata, "1.0.5", "69f1ee029a49afa04ad77801febaf69385f3d3e3d1e4b56b9469025677b89a28", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "55519aa2a99e5d2095c1e61cc74c9be69688f8ab75c27da724eb8279ff402a5a"},
"ueberauth": {:hex, :ueberauth, "0.10.7", "5a31cbe11e7ce5c7484d745dc9e1f11948e89662f8510d03c616de03df581ebd", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "0bccf73e2ffd6337971340832947ba232877aa8122dba4c95be9f729c8987377"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},

View file

@ -0,0 +1,22 @@
defmodule Pleroma.Repo.Migrations.CreateDomains do
use Ecto.Migration
def change do
create_if_not_exists table(:domains) do
add(:domain, :citext)
add(:public, :boolean)
add(:resolves, :boolean)
add(:last_checked_at, :naive_datetime)
timestamps(type: :utc_datetime)
end
create_if_not_exists(unique_index(:domains, [:domain]))
alter table(:users) do
add(:domain_id, references(:domains))
end
create_if_not_exists(index(:users, [:domain_id]))
end
end

View file

@ -387,6 +387,7 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do
["data_migration_failed_ids"],
["data_migrations"],
["deliveries"],
["domains"],
["filters"],
["following_relationships"],
["hashtags"],

View file

@ -841,6 +841,55 @@ defmodule Pleroma.UserTest do
end
end
describe "user registration, with custom domain" do
@full_user_data %{
bio: "A guy",
name: "my name",
nickname: "nick",
password: "test",
password_confirmation: "test",
email: "email@example.com"
}
setup do
clear_config([:instance, :multitenancy], %{enabled: true})
end
test "it registers on a given domain" do
{:ok, %{id: domain_id}} =
Pleroma.Domain.create(%{
domain: "pleroma.example.org",
public: true
})
params =
@full_user_data
|> Map.put(:domain_id, to_string(domain_id))
changeset = User.register_changeset(%User{}, params)
assert changeset.valid?
{:ok, user} = Repo.insert(changeset)
assert user.nickname == "nick@pleroma.example.org"
end
test "it fails when domain is private" do
{:ok, %{id: domain_id}} =
Pleroma.Domain.create(%{
domain: "private.example.org",
public: false
})
params =
@full_user_data
|> Map.put(:domain_id, to_string(domain_id))
changeset = User.register_changeset(%User{}, params)
refute changeset.valid?
end
end
describe "get_or_fetch/1" do
test "gets an existing user by nickname" do
user = insert(:user)

View file

@ -138,6 +138,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
end
test "it returns a json representation of a local user domain different from host", %{
conn: conn
} do
user =
insert(:user, %{
nickname: "nick@example.org"
})
conn =
conn
|> put_req_header("accept", "application/json")
|> get("/users/#{user.nickname}.json")
user = User.get_cached_by_id(user.id)
assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
end
test "it returns 404 for remote users", %{
conn: conn
} do

View file

@ -0,0 +1,166 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.DomainControllerTest do
use Pleroma.Web.ConnCase
import Pleroma.Factory
alias Pleroma.Domain
setup do
clear_config([Pleroma.Web.WebFinger, :domain], "example.com")
admin = insert(:user, is_admin: true)
token = insert(:oauth_admin_token, user: admin)
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, token)
{:ok, %{admin: admin, token: token, conn: conn}}
end
describe "GET /api/pleroma/admin/domains" do
test "list created domains", %{conn: conn} do
_domain =
Domain.create(%{
domain: "pleroma.mkljczk.pl",
public: true
})
_domain =
Domain.create(%{
domain: "pleroma2.mkljczk.pl"
})
conn = get(conn, "/api/pleroma/admin/domains")
[
%{
"id" => _id,
"domain" => "pleroma.mkljczk.pl",
"public" => true
},
%{
"id" => _id2,
"domain" => "pleroma2.mkljczk.pl",
"public" => false
}
] = json_response_and_validate_schema(conn, 200)
end
end
describe "POST /api/pleroma/admin/domains" do
test "create a valid domain", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/admin/domains", %{
domain: "pleroma.mkljczk.pl",
public: true
})
%{
"id" => _id,
"domain" => "pleroma.mkljczk.pl",
"public" => true
} = json_response_and_validate_schema(conn, 200)
end
test "create a domain the same as host", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/admin/domains", %{
domain: "example.com",
public: false
})
%{"error" => "invalid_domain"} = json_response_and_validate_schema(conn, 400)
end
test "create duplicate domains", %{conn: conn} do
Domain.create(%{
domain: "pleroma.mkljczk.pl",
public: true
})
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/admin/domains", %{
domain: "pleroma.mkljczk.pl",
public: false
})
assert json_response_and_validate_schema(conn, 400)
end
end
describe "PATCH /api/pleroma/admin/domains/:id" do
test "update domain privacy", %{conn: conn} do
{:ok, %{id: domain_id}} =
Domain.create(%{
domain: "pleroma.mkljczk.pl",
public: true
})
conn =
conn
|> put_req_header("content-type", "application/json")
|> patch("/api/pleroma/admin/domains/#{domain_id}", %{
public: false
})
%{
"id" => _id,
"domain" => "pleroma.mkljczk.pl",
"public" => false
} = json_response_and_validate_schema(conn, 200)
end
test "doesn't update domain name", %{conn: conn} do
{:ok, %{id: domain_id}} =
Domain.create(%{
domain: "plemora.mkljczk.pl",
public: true
})
conn =
conn
|> put_req_header("content-type", "application/json")
|> patch("/api/pleroma/admin/domains/#{domain_id}", %{
domain: "pleroma.mkljczk.pl"
})
%{
"id" => _id,
"domain" => "plemora.mkljczk.pl",
"public" => true
} = json_response_and_validate_schema(conn, 200)
end
end
describe "DELETE /api/pleroma/admin/domains/:id" do
test "delete a domain", %{conn: conn} do
{:ok, %{id: domain_id}} =
Domain.create(%{
domain: "pleroma.mkljczk.pl",
public: true
})
conn =
conn
|> delete("/api/pleroma/admin/domains/#{domain_id}")
%{} = json_response_and_validate_schema(conn, 200)
domains = Domain.list()
assert length(domains) == 0
end
end
end

View file

@ -194,4 +194,53 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do
refute Map.has_key?(result["pleroma"]["metadata"]["base_urls"], "media_proxy")
refute Map.has_key?(result["pleroma"]["metadata"]["base_urls"], "upload")
end
test "instance domains", %{conn: conn} do
clear_config([:instance, :multitenancy], %{enabled: true})
{:ok, %{id: domain_id}} =
Pleroma.Domain.create(%{
domain: "pleroma.example.org"
})
domain_id = to_string(domain_id)
assert %{
"pleroma" => %{
"metadata" => %{
"multitenancy" => %{
"enabled" => true,
"domains" => [
%{
"id" => "",
"domain" => _,
"public" => true
},
%{
"id" => ^domain_id,
"domain" => "pleroma.example.org",
"public" => false
}
]
}
}
}
} =
conn
|> get("/api/v1/instance")
|> json_response_and_validate_schema(200)
clear_config([:instance, :multitenancy, :enabled], false)
assert %{
"pleroma" => %{
"metadata" => %{
"multitenancy" => nil
}
}
} =
conn
|> get("/api/v1/instance")
|> json_response_and_validate_schema(200)
end
end

View file

@ -407,6 +407,43 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
assert [] = result
end
test "filtering local posts basing on domain", %{conn: conn} do
clear_config([:instance, :multitenancy], %{enabled: true, separate_timelines: false})
{:ok, domain} = Pleroma.Domain.create(%{domain: "pleroma.example.org"})
user1 = insert(:user)
user2 = insert(:user, %{domain_id: domain.id})
%{id: note1} = insert(:note_activity, user: user1)
%{id: note2} = insert(:note_activity, user: user2)
assert [
%{"id" => ^note2},
%{"id" => ^note1}
] =
conn
|> get("/api/v1/timelines/public?local=true")
|> json_response_and_validate_schema(200)
clear_config([:instance, :multitenancy, :separate_timelines], true)
assert [%{"id" => ^note1}] =
conn
|> get("/api/v1/timelines/public?local=true")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^note1}] =
conn
|> get("/api/v1/timelines/public?local=true")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^note2}] =
conn
|> get("http://pleroma.example.org/api/v1/timelines/public?local=true")
|> json_response_and_validate_schema(200)
end
end
defp local_and_remote_activities do

View file

@ -98,7 +98,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
skip_thread_containment: false,
accepts_chat_messages: nil,
avatar_description: "",
header_description: ""
header_description: "",
is_local: true
}
}
@ -344,7 +345,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
skip_thread_containment: false,
accepts_chat_messages: nil,
avatar_description: "",
header_description: ""
header_description: "",
is_local: true
}
}

View file

@ -37,6 +37,20 @@ defmodule Pleroma.Web.WebFingerTest do
{:ok, result} = WebFinger.webfinger(user.ap_id, "XML")
assert is_binary(result)
end
test "works for fqns with domains other than host" do
user = insert(:user, %{nickname: "nick@example.org"})
{:ok, result} = WebFinger.webfinger("#{user.nickname})}", "XML")
assert is_binary(result)
end
test "doesn't work for remote users" do
user = insert(:user, %{local: false})
assert {:error, _} = WebFinger.webfinger("#{user.nickname})}", "XML")
end
end
describe "fingering" do

View file

@ -0,0 +1,118 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.CheckDomainsResolveWorkerTest do
use Pleroma.DataCase
alias Pleroma.Domain
alias Pleroma.Workers.CheckDomainResolveWorker
setup do
Pleroma.Web.Endpoint.config_change(
[{Pleroma.Web.Endpoint, url: [host: "pleroma.example.org", scheme: "https", port: 443]}],
[]
)
clear_config([Pleroma.Web.Endpoint, :url, :host], "pleroma.example.org")
Tesla.Mock.mock_global(fn
%{url: "https://pleroma.example.org/.well-known/host-meta"} ->
%Tesla.Env{
status: 200,
body:
"test/fixtures/webfinger/pleroma-host-meta.xml"
|> File.read!()
|> String.replace("{{domain}}", "pleroma.example.org")
}
%{url: "https://example.org/.well-known/host-meta"} ->
%Tesla.Env{
status: 200,
body:
"test/fixtures/webfinger/pleroma-host-meta.xml"
|> File.read!()
|> String.replace("{{domain}}", "pleroma.example.org")
}
%{url: "https://social.example.org/.well-known/host-meta"} ->
%Tesla.Env{
status: 302,
headers: [{"location", "https://pleroma.example.org/.well-known/host-meta"}]
}
%{url: "https://notpleroma.example.org/.well-known/host-meta"} ->
%Tesla.Env{
status: 200,
body:
"test/fixtures/webfinger/pleroma-host-meta.xml"
|> File.read!()
|> String.replace("{{domain}}", "notpleroma.example.org")
}
%{url: "https://wrong.example.org/.well-known/host-meta"} ->
%Tesla.Env{
status: 302,
headers: [{"location", "https://notpleroma.example.org/.well-known/host-meta"}]
}
%{url: "https://bad.example.org/.well-known/host-meta"} ->
%Tesla.Env{status: 404}
end)
on_exit(fn ->
Pleroma.Web.Endpoint.config_change(
[{Pleroma.Web.Endpoint, url: [host: "localhost"]}],
[]
)
end)
end
test "verifies domain state" do
{:ok, %{id: domain_id}} =
Domain.create(%{
domain: "example.org"
})
{:ok, domain} = CheckDomainResolveWorker.perform(%Oban.Job{args: %{"id" => domain_id}})
assert domain.resolves == true
assert domain.last_checked_at != nil
end
test "verifies domain state for a redirect" do
{:ok, %{id: domain_id}} =
Domain.create(%{
domain: "social.example.org"
})
{:ok, domain} = CheckDomainResolveWorker.perform(%Oban.Job{args: %{"id" => domain_id}})
assert domain.resolves == true
assert domain.last_checked_at != nil
end
test "doesn't verify state for an incorrect redirect" do
{:ok, %{id: domain_id}} =
Domain.create(%{
domain: "wrong.example.org"
})
{:ok, domain} = CheckDomainResolveWorker.perform(%Oban.Job{args: %{"id" => domain_id}})
assert domain.resolves == false
assert domain.last_checked_at != nil
end
test "doesn't verify state for unimplemented redirect" do
{:ok, %{id: domain_id}} =
Domain.create(%{
domain: "bad.example.org"
})
{:ok, domain} = CheckDomainResolveWorker.perform(%Oban.Job{args: %{"id" => domain_id}})
assert domain.resolves == false
assert domain.last_checked_at != nil
end
end