- Nodeinfo keys for unauthenticated timeline visibility
- Option to disable federated timeline
- Option to make the bubble timeline publicly accessible
+- Ability to swap between installed standard frontends
+ - *mastodon frontends are still not counted as standard frontends due to the complexity in serving them correctly*.
## 2023.03
primary: %{"name" => "pleroma-fe", "ref" => "stable"},
admin: %{"name" => "admin-fe", "ref" => "stable"},
mastodon: %{"name" => "mastodon-fe", "ref" => "akkoma"},
+ pickable: [
+ "pleroma-fe/stable"
+ ],
swagger: %{
"name" => "swagger-ui",
"ref" => "stable",
description:
"A map containing available frontends and parameters for their installation.",
children: frontend_options
+ },
+ %{
+ key: :pickable,
+ type: {:list, :string},
+ description:
+ "A list containing all frontends users can pick as their preference, format is :name/:ref, e.g pleroma-fe/stable."
}
]
},
params
)
|> limit(20)
-
+
Ecto.Adapters.SQL.explain(Repo, :all, query, analyze: true, timeout: :infinity)
- |> IO.puts()
+ |> IO.puts()
end
end
alias Pleroma.Activity
alias Pleroma.Delivery
alias Pleroma.Object
- alias Pleroma.Object.Fetcher
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.InternalFetchActor
|> json("Invalid HTTP Signature")
end
- # POST /relay/inbox -or- POST /internal/fetch/inbox
- def inbox(conn, %{"type" => "Create"} = params) do
- if FederatingPlug.federating?() do
- post_inbox_relayed_create(conn, params)
- else
- conn
- |> put_status(:bad_request)
- |> json("Not federating")
- end
- end
-
def inbox(conn, _params) do
conn
|> put_status(:bad_request)
|> json("error, missing HTTP Signature")
end
- defp post_inbox_relayed_create(conn, params) do
- Logger.debug(
- "Signature missing or not from author, relayed Create message, fetching object from source"
- )
-
- Fetcher.fetch_object_from_id(params["object"]["id"])
-
- json(conn, "ok")
- end
-
defp represent_service_actor(%User{} = user, conn) do
conn
|> put_resp_content_type("application/activity+json")
Config.get([:mrf_simple, :reject], [])
end
+ defp allowed_instances do
+ Config.get([:mrf_simple, :accept])
+ end
+
def should_federate?(url) do
%{host: host} = URI.parse(url)
- quarantined_instances =
- blocked_instances()
+ with allowed <- allowed_instances(),
+ false <- Enum.empty?(allowed) do
+ allowed
|> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
|> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
+ |> Pleroma.Web.ActivityPub.MRF.subdomain_match?(host)
+ else
+ _ ->
+ quarantined_instances =
+ blocked_instances()
+ |> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
+ |> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
- !Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
+ not Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
+ end
end
@spec recipients(User.t(), Activity.t()) :: list(User.t()) | []
alias Pleroma.Akkoma.FrontendSettingsProfile
@unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
+
+ plug(
+ OAuthScopesPlug,
+ @unauthenticated_access
+ when action in [
+ :available_frontends,
+ :update_preferred_frontend
+ ]
+ )
+
plug(
OAuthScopesPlug,
%{@unauthenticated_access | scopes: ["read:accounts"]}
|> json(profile.settings)
end
end
+
+ @doc "GET /api/v1/akkoma/preferred_frontend/available"
+ def available_frontends(conn, _params) do
+ available = Pleroma.Config.get([:frontends, :pickable])
+
+ conn
+ |> json(available)
+ end
+
+ @doc "PUT /api/v1/akkoma/preferred_frontend"
+ def update_preferred_frontend(
+ %{body_params: %{frontend_name: preferred_frontend}} = conn,
+ _params
+ ) do
+ conn
+ |> put_resp_cookie("preferred_frontend", preferred_frontend)
+ |> json(%{frontend_name: preferred_frontend})
+ end
end
--- /dev/null
+defmodule Pleroma.Web.AkkomaAPI.FrontendSwitcherController do
+ use Pleroma.Web, :controller
+ alias Pleroma.Config
+
+ @doc "GET /akkoma/frontend"
+ def switch(conn, _params) do
+ pickable = Config.get([:frontends, :pickable], [])
+
+ conn
+ |> put_view(Pleroma.Web.AkkomaAPI.FrontendSwitcherView)
+ |> render("switch.html", choices: pickable)
+ end
+
+ @doc "POST /akkoma/frontend"
+ def do_switch(conn, params) do
+ conn
+ |> put_resp_cookie("preferred_frontend", params["frontend"])
+ |> html("<meta http-equiv=\"refresh\" content=\"0; url=/\">")
+ end
+end
--- /dev/null
+defmodule Pleroma.Web.AkkomaAPI.FrontendSwitcherView do
+ use Pleroma.Web, :view
+end
@spec list_profiles_operation() :: Operation.t()
def list_profiles_operation() do
%Operation{
- tags: ["Retrieve frontend setting profiles"],
+ tags: ["Frontends"],
summary: "Frontend Settings Profiles",
description: "List frontend setting profiles",
operationId: "AkkomaAPI.FrontendSettingsController.list_profiles",
@spec get_profile_operation() :: Operation.t()
def get_profile_operation() do
%Operation{
- tags: ["Retrieve frontend setting profile"],
+ tags: ["Frontends"],
summary: "Frontend Settings Profile",
description: "Get frontend setting profile",
operationId: "AkkomaAPI.FrontendSettingsController.get_profile",
@spec delete_profile_operation() :: Operation.t()
def delete_profile_operation() do
%Operation{
- tags: ["Delete frontend setting profile"],
+ tags: ["Frontends"],
summary: "Delete frontend Settings Profile",
description: "Delete frontend setting profile",
operationId: "AkkomaAPI.FrontendSettingsController.delete_profile",
@spec update_profile_operation() :: Operation.t()
def update_profile_operation() do
%Operation{
- tags: ["Update frontend setting profile"],
+ tags: ["Frontends"],
summary: "Frontend Settings Profile",
description: "Update frontend setting profile",
operationId: "AkkomaAPI.FrontendSettingsController.update_profile_operation",
}
end
+ def available_frontends_operation() do
+ %Operation{
+ tags: ["Frontends"],
+ summary: "Frontend Settings Profiles",
+ description: "List frontend setting profiles",
+ operationId: "AkkomaAPI.FrontendSettingsController.available_frontends",
+ responses: %{
+ 200 =>
+ Operation.response("Frontends", "application/json", %Schema{
+ type: :array,
+ items: %Schema{
+ type: :string
+ }
+ })
+ }
+ }
+ end
+
+ def update_preferred_frontend_operation() do
+ %Operation{
+ tags: ["Frontends"],
+ summary: "Frontend Settings Profiles",
+ description: "List frontend setting profiles",
+ operationId: "AkkomaAPI.FrontendSettingsController.available_frontends",
+ requestBody:
+ request_body(
+ "Frontend",
+ %Schema{
+ type: :object,
+ required: [:frontend_name],
+ properties: %{
+ frontend_name: %Schema{
+ type: :string,
+ description: "Frontend name"
+ }
+ }
+ },
+ required: true
+ ),
+ responses: %{
+ 200 =>
+ Operation.response("Frontends", "application/json", %Schema{
+ type: :array,
+ items: %Schema{
+ type: :string
+ }
+ })
+ }
+ }
+ end
+
def frontend_name_param do
Operation.parameter(:frontend_name, :path, :string, "Frontend name",
example: "pleroma-fe",
def redirector(conn, _params, code \\ 200) do
conn
|> put_resp_content_type("text/html")
- |> send_file(code, index_file_path())
+ |> send_file(code, index_file_path(conn))
end
def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do
end
def redirector_with_meta(conn, params) do
- {:ok, index_content} = File.read(index_file_path())
+ {:ok, index_content} = File.read(index_file_path(conn))
tags = build_tags(conn, params)
preloads = preload_data(conn, params)
end
def redirector_with_preload(conn, params) do
- {:ok, index_content} = File.read(index_file_path())
+ {:ok, index_content} = File.read(index_file_path(conn))
preloads = preload_data(conn, params)
tags = Metadata.build_static_tags(params)
title = "<title>#{Pleroma.Config.get([:instance, :name])}</title>"
|> text("")
end
- defp index_file_path do
- Pleroma.Web.Plugs.InstanceStatic.file_path("index.html")
+ defp index_file_path(conn) do
+ frontend_type = Pleroma.Web.Plugs.FrontendStatic.preferred_or_fallback(conn, :primary)
+ Pleroma.Web.Plugs.InstanceStatic.file_path("index.html", frontend_type)
end
defp build_tags(conn, params) do
defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
use Pleroma.Web, :controller
- alias Pleroma.Config
- alias Pleroma.Stats
- alias Pleroma.User
- alias Pleroma.Web.Federator.Publisher
- alias Pleroma.Web.MastodonAPI.InstanceView
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Nodeinfo.Nodeinfo
defmodule Pleroma.Web.Plugs.FrontendStatic do
require Pleroma.Constants
+ @frontend_cookie_name "preferred_frontend"
+
@moduledoc """
This is a shim to call `Plug.Static` but with runtime `from` configuration`. It dispatches to the different frontends.
"""
@behaviour Plug
- def file_path(path, frontend_type \\ :primary) do
- if configuration = Pleroma.Config.get([:frontends, frontend_type]) do
- instance_static_path = Pleroma.Config.get([:instance, :static_dir], "instance/static")
+ defp instance_static_path do
+ Pleroma.Config.get([:instance, :static_dir], "instance/static")
+ end
+
+ def file_path(path, frontend_type \\ :primary)
+ def file_path(path, frontend_type) when is_atom(frontend_type) do
+ if configuration = Pleroma.Config.get([:frontends, frontend_type]) do
Path.join([
- instance_static_path,
+ instance_static_path(),
"frontends",
configuration["name"],
configuration["ref"],
end
end
+ def file_path(path, frontend_type) when is_binary(frontend_type) do
+ Path.join([
+ instance_static_path(),
+ "frontends",
+ frontend_type,
+ path
+ ])
+ end
+
def init(opts) do
opts
|> Keyword.put(:from, "__unconfigured_frontend_static_plug")
with false <- api_route?(conn.path_info),
false <- invalid_path?(conn.path_info),
true <- enabled?(opts[:if]),
- frontend_type <- Map.get(opts, :frontend_type, :primary),
+ fallback_frontend_type <- Map.get(opts, :frontend_type, :primary),
+ frontend_type <- preferred_or_fallback(conn, fallback_frontend_type),
path when not is_nil(path) <- file_path("", frontend_type) do
call_static(conn, opts, path)
else
end
end
+ def preferred_frontend(conn) do
+ %{req_cookies: cookies} =
+ conn
+ |> Plug.Conn.fetch_cookies()
+
+ Map.get(cookies, @frontend_cookie_name)
+ end
+
+ # Only override primary frontend
+ def preferred_or_fallback(conn, :primary) do
+ case preferred_frontend(conn) do
+ nil ->
+ :primary
+
+ frontend ->
+ if Enum.member?(Pleroma.Config.get([:frontends, :pickable], []), frontend) do
+ frontend
+ else
+ :primary
+ end
+ end
+ end
+
+ def preferred_or_fallback(_conn, fallback), do: fallback
+
defp enabled?(if_opt) when is_function(if_opt), do: if_opt.()
defp enabled?(true), do: true
defp enabled?(_), do: false
require Logger
+ @mix_env Mix.env()
+
def init(opts), do: opts
def call(conn, _options) do
style_src = "style-src 'self' '#{nonce_tag}'"
font_src = "font-src 'self'"
- script_src = "script-src 'self' '#{nonce_tag}'"
+ script_src = "script-src 'self' '#{nonce_tag}' "
+
+ script_src =
+ if @mix_env == :dev do
+ "script-src 'self' 'unsafe-eval' 'unsafe-inline'"
+ else
+ script_src
+ end
report = if report_uri, do: ["report-uri ", report_uri, ";report-to csp-endpoint"]
insecure = if scheme == "https", do: "upgrade-insecure-requests"
"""
@behaviour Plug
- def file_path(path) do
+ def file_path(path, frontend_type \\ :primary) do
instance_path =
Path.join(Pleroma.Config.get([:instance, :static_dir], "instance/static/"), path)
- frontend_path = Pleroma.Web.Plugs.FrontendStatic.file_path(path, :primary)
+ frontend_path = Pleroma.Web.Plugs.FrontendStatic.file_path(path, frontend_type)
(File.exists?(instance_path) && instance_path) ||
(frontend_path && File.exists?(frontend_path) && frontend_path) ||
put("/statuses/:id/emoji_reactions/:emoji", EmojiReactionController, :create)
end
+ scope "/akkoma/", Pleroma.Web.AkkomaAPI do
+ pipe_through(:browser)
+
+ get("/frontend", FrontendSwitcherController, :switch)
+ post("/frontend", FrontendSwitcherController, :do_switch)
+ end
+
+ scope "/api/v1/akkoma", Pleroma.Web.AkkomaAPI do
+ pipe_through(:api)
+
+ get(
+ "/api/v1/akkoma/preferred_frontend/available",
+ FrontendSettingsController,
+ :available_frontends
+ )
+
+ put(
+ "/api/v1/akkoma/preferred_frontend",
+ FrontendSettingsController,
+ :update_preferred_frontend
+ )
+ end
+
scope "/api/v1/akkoma", Pleroma.Web.AkkomaAPI do
pipe_through(:authenticated_api)
get("/metrics", MetricsController, :show)
--- /dev/null
+<h2>Switch Frontend</h2>
+
+<h3>After you submit, you will need to refresh manually to get your new frontend!</h3>
+
+<%= form_for @conn, Routes.frontend_switcher_path(@conn, :do_switch), fn f -> %>
+ <%= select(f, :frontend, @choices) %>
+
+ <%= submit do: "submit" %>
+<% end %>
+
assert_receive {:mix_shell, :info, ["https://relay.mastodon.host/actor"]}
end
- @tag capture_log: true
- test "without valid signature, " <>
- "it only accepts Create activities and requires enabled federation",
- %{conn: conn} do
- data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
- non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Jason.decode!()
-
- conn = put_req_header(conn, "content-type", "application/activity+json")
-
- clear_config([:instance, :federating], false)
-
- conn
- |> post("/inbox", data)
- |> json_response(403)
-
- conn
- |> post("/inbox", non_create_data)
- |> json_response(403)
-
- clear_config([:instance, :federating], true)
-
- ret_conn = post(conn, "/inbox", data)
- assert "ok" == json_response(ret_conn, 200)
-
- conn
- |> post("/inbox", non_create_data)
- |> json_response(400)
- end
-
test "accepts Add/Remove activities", %{conn: conn} do
object_id = "c61d6733-e256-4fe1-ab13-1e369789423f"
test "should not return 404 if local is specified" do
clear_config([:instance, :federated_timeline_available], false)
- result =
- build_conn()
- |> get("/api/v1/timelines/public?local=true")
- |> json_response_and_validate_schema(200)
+ build_conn()
+ |> get("/api/v1/timelines/public?local=true")
+ |> json_response_and_validate_schema(200)
end
end
"main",
"ostatus_subscribe",
"oauth",
+ "akkoma",
"objects",
"activities",
"notice",
assert %{valid_signature: false} == conn.assigns
end
+ test "allowlist federation: it considers a mapped identity to be valid when the associated instance is allowed" do
+ clear_config([:activitypub, :authorized_fetch_mode], true)
+
+ clear_config([:mrf_simple, :accept], [
+ {"mastodon.example.org", "anime is allowed"}
+ ])
+
+ on_exit(fn ->
+ Pleroma.Config.put([:activitypub, :authorized_fetch_mode], false)
+ Pleroma.Config.put([:mrf_simple, :accept], [])
+ end)
+
+ conn =
+ build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
+ |> set_signature("http://mastodon.example.org/users/admin")
+ |> MappedSignatureToIdentityPlug.call(%{})
+
+ assert conn.assigns[:valid_signature]
+ refute is_nil(conn.assigns.user)
+ end
+
+ test "allowlist federation: it considers a mapped identity to be invalid when the associated instance is not allowed" do
+ clear_config([:activitypub, :authorized_fetch_mode], true)
+
+ clear_config([:mrf_simple, :accept], [
+ {"misskey.example.org", "anime is allowed"}
+ ])
+
+ on_exit(fn ->
+ Pleroma.Config.put([:activitypub, :authorized_fetch_mode], false)
+ Pleroma.Config.put([:mrf_simple, :accept], [])
+ end)
+
+ conn =
+ build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
+ |> set_signature("http://mastodon.example.org/users/admin")
+ |> MappedSignatureToIdentityPlug.call(%{})
+
+ assert %{valid_signature: false} == conn.assigns
+ end
+
@tag skip: "known breakage; the testsuite presently depends on it"
test "it considers a mapped identity to be invalid when the identity cannot be found" do
conn =