Merge branch 'develop' into webhooks
This commit is contained in:
commit
83dcc4ae36
59 changed files with 2245 additions and 148 deletions
|
|
@ -127,6 +127,13 @@ defmodule Pleroma.Formatter do
|
|||
Earmark.as_html!(text, %Earmark.Options{compact_output: true, smartypants: false})
|
||||
end
|
||||
|
||||
def markdown_to_html(text, opts) do
|
||||
Earmark.as_html!(
|
||||
text,
|
||||
%Earmark.Options{compact_output: true, smartypants: false} |> Map.merge(opts)
|
||||
)
|
||||
end
|
||||
|
||||
def html_escape({text, mentions, hashtags}, type) do
|
||||
{html_escape(text, type), mentions, hashtags}
|
||||
end
|
||||
|
|
@ -135,6 +142,10 @@ defmodule Pleroma.Formatter do
|
|||
HTML.filter_tags(text)
|
||||
end
|
||||
|
||||
def html_escape(text, "text/x.misskeymarkdown") do
|
||||
HTML.filter_tags(text)
|
||||
end
|
||||
|
||||
def html_escape(text, "text/plain") do
|
||||
Regex.split(@link_regex, text, include_captures: true)
|
||||
|> Enum.map_every(2, fn chunk ->
|
||||
|
|
|
|||
|
|
@ -75,8 +75,8 @@ defmodule Pleroma.Frontend do
|
|||
end
|
||||
|
||||
defp download_build(frontend_info, dest) do
|
||||
Logger.info("Downloading pre-built bundle for #{frontend_info["name"]}")
|
||||
url = String.replace(frontend_info["build_url"], "${ref}", frontend_info["ref"])
|
||||
Logger.info("Downloading pre-built bundle for #{frontend_info["name"]} from #{url}")
|
||||
|
||||
with {:ok, %{status: 200, body: zip_body}} <-
|
||||
Pleroma.HTTP.get(url, [], pool: :media, recv_timeout: 120_000) do
|
||||
|
|
|
|||
|
|
@ -372,13 +372,21 @@ defmodule Pleroma.Object do
|
|||
option
|
||||
end)
|
||||
|
||||
voters = [actor | object.data["voters"] || []] |> Enum.uniq()
|
||||
existing_voters = object.data["voters"] || []
|
||||
voters = [actor | existing_voters] |> Enum.uniq()
|
||||
new_voter? = actor not in existing_voters
|
||||
existing_voters_count = object.data["votersCount"]
|
||||
|
||||
voters_count =
|
||||
if Map.has_key?(object.data, "votersCount") do
|
||||
object.data["votersCount"] + 1
|
||||
else
|
||||
length(voters)
|
||||
cond do
|
||||
is_integer(existing_voters_count) and new_voter? ->
|
||||
existing_voters_count + 1
|
||||
|
||||
is_integer(existing_voters_count) ->
|
||||
existing_voters_count
|
||||
|
||||
true ->
|
||||
length(voters)
|
||||
end
|
||||
|
||||
data =
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ defmodule Pleroma.Signature do
|
|||
|> put_req_header("(request-target)", request_target)
|
||||
|> put_req_header("@request-target", request_target)
|
||||
|
||||
@http_signatures_impl.validate_conn(conn)
|
||||
@http_signatures_impl.validate_conn(conn) == true
|
||||
end
|
||||
|
||||
@spec validate_signature(Plug.Conn.t()) :: boolean()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
defmodule Pleroma.User.Search do
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators.Uri, as: UriType
|
||||
alias Pleroma.Instances.Instance
|
||||
alias Pleroma.Pagination
|
||||
alias Pleroma.User
|
||||
|
||||
|
|
@ -88,12 +89,13 @@ defmodule Pleroma.User.Search do
|
|||
|> filter_invisible_users()
|
||||
|> filter_internal_users()
|
||||
|> filter_blocked_domains(for_user)
|
||||
|> filter_unreachable_users()
|
||||
|> fts_search(query_string)
|
||||
|> select_top_users(top_user_ids)
|
||||
|> trigram_rank(query_string)
|
||||
|> boost_search_rank(for_user, top_user_ids)
|
||||
|> subquery()
|
||||
|> order_by(desc: :search_rank)
|
||||
|> order_by_search_rank(for_user)
|
||||
|> maybe_restrict_local(for_user)
|
||||
|> maybe_restrict_accepting_chat_messages(capabilities)
|
||||
|> filter_deactivated_users()
|
||||
|
|
@ -196,6 +198,14 @@ defmodule Pleroma.User.Search do
|
|||
|
||||
defp filter_blocked_domains(query, _), do: query
|
||||
|
||||
defp filter_unreachable_users(query) do
|
||||
from(u in query,
|
||||
left_join: i in Instance,
|
||||
on: i.host == fragment("substring(? from '.*://([^/]*)')", u.ap_id),
|
||||
where: is_nil(i.unreachable_since)
|
||||
)
|
||||
end
|
||||
|
||||
defp maybe_resolve(true, user, query) do
|
||||
case {limit(), user} do
|
||||
{:all, _} -> :noop
|
||||
|
|
@ -236,6 +246,16 @@ defmodule Pleroma.User.Search do
|
|||
|
||||
from(u in subquery(query),
|
||||
select_merge: %{
|
||||
search_type:
|
||||
fragment(
|
||||
"""
|
||||
CASE WHEN (?) THEN 2
|
||||
WHEN (?) THEN 1
|
||||
ELSE 0 END
|
||||
""",
|
||||
u.id in ^top_user_ids,
|
||||
u.id in ^friends_ids or u.id in ^followers_ids
|
||||
),
|
||||
search_rank:
|
||||
fragment(
|
||||
"""
|
||||
|
|
@ -261,6 +281,14 @@ defmodule Pleroma.User.Search do
|
|||
defp boost_search_rank(query, _for_user, top_user_ids) do
|
||||
from(u in subquery(query),
|
||||
select_merge: %{
|
||||
search_type:
|
||||
fragment(
|
||||
"""
|
||||
CASE WHEN (?) THEN 2
|
||||
ELSE 0 END
|
||||
""",
|
||||
u.id in ^top_user_ids
|
||||
),
|
||||
search_rank:
|
||||
fragment(
|
||||
"""
|
||||
|
|
@ -273,4 +301,22 @@ defmodule Pleroma.User.Search do
|
|||
}
|
||||
)
|
||||
end
|
||||
|
||||
defp order_by_search_rank(query, %User{}) do
|
||||
order_by(
|
||||
query,
|
||||
[u],
|
||||
desc: u.search_type,
|
||||
desc_nulls_last:
|
||||
fragment(
|
||||
"CASE WHEN ? = 1 THEN COALESCE(?, ?) ELSE NULL END",
|
||||
u.search_type,
|
||||
u.last_status_at,
|
||||
u.last_active_at
|
||||
),
|
||||
desc: u.search_rank
|
||||
)
|
||||
end
|
||||
|
||||
defp order_by_search_rank(query, _), do: order_by(query, desc: :search_rank)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ defmodule Pleroma.Web do
|
|||
|
||||
def controller do
|
||||
quote do
|
||||
use Phoenix.Controller, namespace: Pleroma.Web
|
||||
use Phoenix.Controller, formats: [json: "View", html: "View"]
|
||||
|
||||
import Plug.Conn
|
||||
|
||||
|
|
@ -42,7 +42,10 @@ defmodule Pleroma.Web do
|
|||
plug(:set_put_layout)
|
||||
|
||||
defp set_put_layout(conn, _) do
|
||||
put_layout(conn, Pleroma.Config.get(:app_layout, "app.html"))
|
||||
case Pleroma.Config.get(:app_layout, "app.html") do
|
||||
false -> put_layout(conn, false)
|
||||
layout -> put_layout(conn, {Pleroma.Web.LayoutView, layout})
|
||||
end
|
||||
end
|
||||
|
||||
# Marks plugs intentionally skipped and blocks their execution if present in plugs chain
|
||||
|
|
|
|||
|
|
@ -303,7 +303,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
end
|
||||
end
|
||||
|
||||
def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
|
||||
def inbox(
|
||||
%{
|
||||
assigns: %{valid_signature: true, valid_host_header: true}
|
||||
} = conn,
|
||||
%{"nickname" => nickname} = params
|
||||
) do
|
||||
with {:recipient_exists, %User{} = recipient} <-
|
||||
{:recipient_exists, User.get_cached_by_nickname(nickname)},
|
||||
{:sender_exists, {:ok, %User{} = actor}} <-
|
||||
|
|
@ -342,7 +347,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
end
|
||||
end
|
||||
|
||||
def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
|
||||
def inbox(%{assigns: %{valid_signature: true, valid_host_header: true}} = conn, params) do
|
||||
Federator.incoming_ap_doc(params)
|
||||
json(conn, "ok")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,9 +6,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|
|||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
|
|
@ -26,6 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|
|||
end
|
||||
|
||||
field(:replies, {:array, ObjectValidators.ObjectID}, default: [])
|
||||
field(:source, :map)
|
||||
end
|
||||
|
||||
def cast_and_apply(data) do
|
||||
|
|
@ -80,6 +84,113 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|
|||
|
||||
def fix_attachments(data), do: data
|
||||
|
||||
defp remote_mention_resolver(
|
||||
%{"id" => ap_id, "tag" => tags},
|
||||
"@" <> nickname = mention,
|
||||
buffer,
|
||||
opts,
|
||||
acc
|
||||
)
|
||||
when is_binary(ap_id) and is_list(tags) do
|
||||
initial_host =
|
||||
ap_id
|
||||
|> URI.parse()
|
||||
|> Map.get(:host)
|
||||
|
||||
with mention_tag when not is_nil(mention_tag) <-
|
||||
Enum.find(tags, &mention_tag?(&1, mention, initial_host)),
|
||||
href when is_binary(href) <- mention_tag["href"],
|
||||
%User{} = user <- User.get_cached_by_ap_id(href) do
|
||||
link = Pleroma.Formatter.mention_from_user(user, opts)
|
||||
{link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}}
|
||||
else
|
||||
_ -> {buffer, acc}
|
||||
end
|
||||
end
|
||||
|
||||
defp remote_mention_resolver(_object, _mention, buffer, _opts, acc), do: {buffer, acc}
|
||||
|
||||
defp mention_tag?(%{"type" => "Mention", "name" => name}, mention, initial_host)
|
||||
when is_binary(name) do
|
||||
name == mention || mention == "#{name}@#{initial_host}"
|
||||
end
|
||||
|
||||
defp mention_tag?(_tag, _mention, _initial_host), do: false
|
||||
|
||||
defp scrub_content(%{"content" => content} = object) when is_binary(content) do
|
||||
Map.put(object, "content", HTML.filter_tags(content))
|
||||
end
|
||||
|
||||
defp scrub_content(object), do: object
|
||||
|
||||
defp mfm_parse_limit do
|
||||
min(Pleroma.Config.get([:instance, :limit]), Pleroma.Config.get([:instance, :remote_limit]))
|
||||
end
|
||||
|
||||
defp normalize_source(%{"source" => source} = object) when is_binary(source) do
|
||||
object
|
||||
|> Map.put("source", %{"content" => source})
|
||||
|> normalize_source()
|
||||
end
|
||||
|
||||
defp normalize_source(%{"source" => source} = object) when is_map(source) do
|
||||
source =
|
||||
case source["content"] do
|
||||
content when is_binary(content) ->
|
||||
if String.length(content) <= mfm_parse_limit() do
|
||||
source
|
||||
else
|
||||
Map.delete(source, "content")
|
||||
end
|
||||
|
||||
nil ->
|
||||
source
|
||||
|
||||
_ ->
|
||||
Map.delete(source, "content")
|
||||
end
|
||||
|
||||
Map.put(object, "source", source)
|
||||
end
|
||||
|
||||
defp normalize_source(object), do: object
|
||||
|
||||
defp fix_misskey_content(%{"htmlMfm" => true, "content" => content} = object)
|
||||
when is_binary(content) do
|
||||
Map.put(object, "content", HTML.filter_tags(content))
|
||||
end
|
||||
|
||||
defp fix_misskey_content(%{"htmlMfm" => true} = object), do: object
|
||||
|
||||
defp fix_misskey_content(
|
||||
%{"source" => %{"mediaType" => "text/x.misskeymarkdown", "content" => content}} = object
|
||||
)
|
||||
when is_binary(content) do
|
||||
mention_handler = fn nick, buffer, opts, acc ->
|
||||
remote_mention_resolver(object, nick, buffer, opts, acc)
|
||||
end
|
||||
|
||||
{linked, _mentions, _tags} =
|
||||
Utils.format_input(content, "text/x.misskeymarkdown", mention_handler: mention_handler)
|
||||
|
||||
Map.put(object, "content", linked)
|
||||
end
|
||||
|
||||
defp fix_misskey_content(%{"source" => %{"mediaType" => "text/x.misskeymarkdown"}} = object),
|
||||
do: scrub_content(object)
|
||||
|
||||
defp fix_misskey_content(%{"_misskey_content" => content} = object) when is_binary(content) do
|
||||
object
|
||||
|> Map.put("source", %{
|
||||
"content" => content,
|
||||
"mediaType" => "text/x.misskeymarkdown"
|
||||
})
|
||||
|> Map.delete("_misskey_content")
|
||||
|> fix_misskey_content()
|
||||
end
|
||||
|
||||
defp fix_misskey_content(object), do: object
|
||||
|
||||
defp fix(data) do
|
||||
data
|
||||
|> CommonFixes.fix_actor()
|
||||
|
|
@ -88,6 +199,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|
|||
|> fix_tag()
|
||||
|> fix_replies()
|
||||
|> fix_attachments()
|
||||
|> normalize_source()
|
||||
|> fix_misskey_content()
|
||||
|> CommonFixes.fix_quote_url()
|
||||
|> CommonFixes.fix_likes()
|
||||
|> Transmogrifier.fix_emoji()
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do
|
|||
quote bind_quoted: binding() do
|
||||
field(:content, :string)
|
||||
field(:contentMap, ObjectValidators.ContentLanguageMap)
|
||||
field(:htmlMfm, :boolean)
|
||||
|
||||
field(:published, ObjectValidators.DateTime)
|
||||
field(:updated, ObjectValidators.DateTime)
|
||||
|
|
|
|||
|
|
@ -430,6 +430,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end)
|
||||
end
|
||||
|
||||
defp reject_third_party_report(%User{local: false}, %User{local: false} = account) do
|
||||
{:reject, "[Transmogrifier] third-party report: #{account.ap_id}"}
|
||||
end
|
||||
|
||||
defp reject_third_party_report(_, _), do: :ok
|
||||
|
||||
def handle_incoming(data, options \\ []) do
|
||||
data
|
||||
|> fix_recursive(&strip_internal_fields/1)
|
||||
|
|
@ -444,9 +450,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
) do
|
||||
with context <- data["context"] || Utils.generate_context_id(),
|
||||
content <- data["content"] || "",
|
||||
objects <- List.wrap(objects),
|
||||
%User{} = actor <- User.get_cached_by_ap_id(actor),
|
||||
# Reduce the object list to find the reported user.
|
||||
%User{} = account <- get_reported(objects),
|
||||
:ok <- reject_third_party_report(actor, account),
|
||||
# Remove the reported user from the object list.
|
||||
statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
|
||||
%{
|
||||
|
|
|
|||
|
|
@ -120,7 +120,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
"https://www.w3.org/ns/activitystreams",
|
||||
"#{Endpoint.url()}/schemas/litepub-0.1.jsonld",
|
||||
%{
|
||||
"@language" => get_language(data)
|
||||
"@language" => get_language(data),
|
||||
"htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -317,6 +317,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
|
||||
emoji = Map.merge(emoji, summary_emoji)
|
||||
|
||||
media_type = Utils.get_content_type(draft.params[:content_type])
|
||||
{:ok, note_data, _meta} = Builder.note(draft)
|
||||
|
||||
object =
|
||||
|
|
@ -324,14 +325,18 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
|> Map.put("emoji", emoji)
|
||||
|> Map.put("source", %{
|
||||
"content" => draft.status,
|
||||
"mediaType" => Utils.get_content_type(draft.params[:content_type])
|
||||
"mediaType" => media_type
|
||||
})
|
||||
|> maybe_put("htmlMfm", true, media_type == "text/x.misskeymarkdown")
|
||||
|> Map.put("generator", draft.params[:generator])
|
||||
|> Map.put("language", draft.language)
|
||||
|
||||
%{draft | object: object}
|
||||
end
|
||||
|
||||
defp maybe_put(map, key, value, true), do: Map.put(map, key, value)
|
||||
defp maybe_put(map, _key, _value, _condition), do: map
|
||||
|
||||
defp preview?(%__MODULE__{} = draft) do
|
||||
preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview])
|
||||
%{draft | preview?: preview?}
|
||||
|
|
|
|||
|
|
@ -322,6 +322,14 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
|> Formatter.linkify(options)
|
||||
end
|
||||
|
||||
def format_input(text, "text/x.misskeymarkdown", options) do
|
||||
text
|
||||
|> Formatter.markdown_to_html(%{breaks: true})
|
||||
|> safe_mfm_to_html()
|
||||
|> Formatter.linkify(options)
|
||||
|> Formatter.html_escape("text/x.misskeymarkdown")
|
||||
end
|
||||
|
||||
def format_input(text, "text/markdown", options) do
|
||||
text
|
||||
|> Formatter.mentions_escape(options)
|
||||
|
|
@ -330,6 +338,16 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
|> Formatter.html_escape("text/html")
|
||||
end
|
||||
|
||||
defp safe_mfm_to_html(html) do
|
||||
html
|
||||
|> MfmParser.Parser.parse()
|
||||
|> MfmParser.Encoder.to_html()
|
||||
rescue
|
||||
_ -> html
|
||||
catch
|
||||
_, _ -> html
|
||||
end
|
||||
|
||||
def format_naive_asctime(date) do
|
||||
date |> DateTime.from_naive!("Etc/UTC") |> format_asctime
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ defmodule Pleroma.Web.Endpoint do
|
|||
websocket: [
|
||||
path: "/",
|
||||
compress: false,
|
||||
connect_info: [:sec_websocket_protocol],
|
||||
connect_info: [:sec_websocket_headers],
|
||||
error_handler: {Pleroma.Web.MastodonAPI.WebsocketHandler, :handle_error, []},
|
||||
fullsweep_after: 20
|
||||
]
|
||||
|
|
|
|||
|
|
@ -71,12 +71,12 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
|
|||
end)
|
||||
end
|
||||
|
||||
defp voters_count(%{data: %{"votersCount" => voters}}) when is_integer(voters), do: voters
|
||||
|
||||
defp voters_count(%{data: %{"voters" => [_ | _] = voters}}) do
|
||||
length(voters)
|
||||
end
|
||||
|
||||
defp voters_count(%{data: %{"votersCount" => voters}}), do: voters
|
||||
|
||||
defp voters_count(_), do: 0
|
||||
|
||||
defp voted_and_own_votes(%{object: object} = params, options) do
|
||||
|
|
|
|||
|
|
@ -245,12 +245,20 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
|
|||
Plug.Conn.send_resp(conn, 404, "Not Found")
|
||||
end
|
||||
|
||||
defp find_access_token(%{
|
||||
connect_info: %{sec_websocket_protocol: [token]}
|
||||
}),
|
||||
do: token
|
||||
defp find_access_token(%{connect_info: %{sec_websocket_headers: sec_headers}} = transport_info) do
|
||||
find_sec_websocket_protocol(sec_headers) || find_access_token_from_params(transport_info)
|
||||
end
|
||||
|
||||
defp find_access_token(%{params: %{"access_token" => token}}), do: token
|
||||
defp find_access_token(transport_info), do: find_access_token_from_params(transport_info)
|
||||
|
||||
defp find_access_token(_), do: nil
|
||||
defp find_sec_websocket_protocol(sec_headers) do
|
||||
Enum.find_value(sec_headers, fn
|
||||
{"sec-websocket-protocol", token} -> token
|
||||
_ -> nil
|
||||
end)
|
||||
end
|
||||
|
||||
defp find_access_token_from_params(%{params: %{"access_token" => token}}), do: token
|
||||
|
||||
defp find_access_token_from_params(_), do: nil
|
||||
end
|
||||
|
|
|
|||
63
lib/pleroma/web/plugs/ensure_host_matches_plug.ex
Normal file
63
lib/pleroma/web/plugs/ensure_host_matches_plug.ex
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlug do
|
||||
@moduledoc "Ensures Host header matches instance"
|
||||
|
||||
alias Pleroma.Web.Endpoint
|
||||
|
||||
import Plug.Conn
|
||||
|
||||
def init(options), do: options
|
||||
|
||||
@spec call(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()
|
||||
def call(%Plug.Conn{assigns: %{valid_signature: true}} = conn, _opts) do
|
||||
# Host header is scheme-less, URI.parse needs the //
|
||||
host_header = get_req_header(conn, "host")
|
||||
host_uri = URI.parse("//#{host_header}")
|
||||
instance_uri = URI.parse(Endpoint.url())
|
||||
|
||||
case host_header do
|
||||
[host] ->
|
||||
cond do
|
||||
host == "" ->
|
||||
resp(conn, 400, "Host header not provided") |> halt()
|
||||
|
||||
true ->
|
||||
if host_matches?(host_uri, instance_uri),
|
||||
do: assign(conn, :valid_host_header, true),
|
||||
else: resp(conn, 400, "Host header does not match this instance") |> halt()
|
||||
end
|
||||
|
||||
[_head | _rest] ->
|
||||
conn
|
||||
|> resp(400, "More than one Host header provided")
|
||||
|> halt()
|
||||
|
||||
[] ->
|
||||
conn
|
||||
|> resp(400, "Host header not provided")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
||||
# Host header may not be provided, but signature verification failed anyway
|
||||
def call(conn, _opts), do: conn
|
||||
|
||||
defp case_insensitive_compare(checked, authority) do
|
||||
String.downcase(checked) == String.downcase(authority)
|
||||
end
|
||||
|
||||
# Host header did not provide port
|
||||
# Host header is scheme-less, URI.parse does not provide default port
|
||||
defp host_matches?(%URI{host: req_host, port: nil}, %URI{host: instance_host}),
|
||||
do: case_insensitive_compare(req_host, instance_host)
|
||||
|
||||
# Host header provided a port
|
||||
# Any port specified in the Endpoint url configuration is valid here
|
||||
defp host_matches?(%URI{host: req_host, port: port}, %URI{host: instance_host, port: port}),
|
||||
do: case_insensitive_compare(req_host, instance_host)
|
||||
|
||||
defp host_matches?(_, _), do: false
|
||||
end
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
defmodule Pleroma.Web.Plugs.RemoteIp do
|
||||
@moduledoc """
|
||||
This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
|
||||
This is a shim to call [`RemoteIp`](https://hex.pm/packages/remote_ip) but with runtime configuration.
|
||||
"""
|
||||
|
||||
alias Pleroma.Config
|
||||
|
|
@ -17,15 +17,29 @@ defmodule Pleroma.Web.Plugs.RemoteIp do
|
|||
|
||||
def call(%{remote_ip: original_remote_ip} = conn, _) do
|
||||
if Config.get([__MODULE__, :enabled]) do
|
||||
%{remote_ip: new_remote_ip} = conn = RemoteIp.call(conn, remote_ip_opts())
|
||||
new_remote_ip = remote_ip(conn) || original_remote_ip
|
||||
|
||||
conn = %{conn | remote_ip: new_remote_ip}
|
||||
assign(conn, :remote_ip_found, original_remote_ip != new_remote_ip)
|
||||
else
|
||||
conn
|
||||
end
|
||||
end
|
||||
|
||||
defp remote_ip(conn) do
|
||||
opts = remote_ip_opts()
|
||||
|
||||
# Do not use RemoteIp.from/2 here: upstream remote_ip always applies its
|
||||
# built-in reserved ranges. Pleroma keeps :reserved configurable, so reuse
|
||||
# only the header parsing and apply Pleroma's own block classification.
|
||||
conn.req_headers
|
||||
|> RemoteIp.Headers.take(opts[:headers])
|
||||
|> RemoteIp.Headers.parse()
|
||||
|> Enum.reverse()
|
||||
|> Enum.find(&client?(&1, opts))
|
||||
end
|
||||
|
||||
defp remote_ip_opts do
|
||||
headers = Config.get([__MODULE__, :headers], []) |> MapSet.new()
|
||||
reserved = Config.get([__MODULE__, :reserved], [])
|
||||
|
||||
proxies =
|
||||
|
|
@ -33,6 +47,26 @@ defmodule Pleroma.Web.Plugs.RemoteIp do
|
|||
|> Enum.concat(reserved)
|
||||
|> Enum.map(&InetHelper.parse_cidr/1)
|
||||
|
||||
{headers, proxies}
|
||||
clients =
|
||||
Config.get([__MODULE__, :clients], [])
|
||||
|> Enum.map(&InetHelper.parse_cidr/1)
|
||||
|
||||
[
|
||||
headers: Config.get([__MODULE__, :headers], []),
|
||||
clients: clients,
|
||||
proxies: proxies
|
||||
]
|
||||
end
|
||||
|
||||
defp client?(ip, opts) do
|
||||
client_ip?(ip, opts[:clients]) || !proxy_ip?(ip, opts[:proxies])
|
||||
end
|
||||
|
||||
defp client_ip?(ip, clients) do
|
||||
Enum.any?(clients, &InetCidr.contains?(&1, ip))
|
||||
end
|
||||
|
||||
defp proxy_ip?(ip, proxies) do
|
||||
Enum.any?(proxies, &InetCidr.contains?(&1, ip))
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -216,6 +216,7 @@ defmodule Pleroma.Web.Router do
|
|||
pipeline :http_signature do
|
||||
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
|
||||
plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
|
||||
plug(Pleroma.Web.Plugs.EnsureHostMatchesPlug)
|
||||
end
|
||||
|
||||
pipeline :inbox_guard do
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.Workers.SignatureRetryWorker do
|
|||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.Federator
|
||||
alias Pleroma.Web.Plugs.EnsureHostMatchesPlug
|
||||
alias Pleroma.Web.Plugs.MappedSignatureToIdentityPlug
|
||||
|
||||
require Logger
|
||||
|
|
@ -48,6 +49,7 @@ defmodule Pleroma.Workers.SignatureRetryWorker do
|
|||
{:ok, _public_key} <- Signature.refetch_public_key(conn_data),
|
||||
{:signature, true} <- {:signature, validate_signature(conn_data)},
|
||||
{:same_actor, true} <- {:same_actor, validate_same_actor(conn_data)},
|
||||
{:host_header, true} <- {:host_header, validate_host_header(conn_data)},
|
||||
{:ok, res} <- Federator.perform(:incoming_ap_doc, params) do
|
||||
unless Instances.reachable?(params["actor"]) do
|
||||
domain = URI.parse(params["actor"]).host
|
||||
|
|
@ -103,6 +105,16 @@ defmodule Pleroma.Workers.SignatureRetryWorker do
|
|||
end
|
||||
end
|
||||
|
||||
defp validate_host_header(conn_data) do
|
||||
case EnsureHostMatchesPlug.call(conn_data, []) do
|
||||
%Plug.Conn{assigns: %{valid_signature: true, valid_host_header: true}} ->
|
||||
true
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_same_actor(conn_data) do
|
||||
case MappedSignatureToIdentityPlug.call(conn_data, []) do
|
||||
%Plug.Conn{assigns: %{valid_signature: true}} ->
|
||||
|
|
@ -170,6 +182,10 @@ defmodule Pleroma.Workers.SignatureRetryWorker do
|
|||
{:same_actor, false} ->
|
||||
{:cancel, :actor_signature_mismatch}
|
||||
|
||||
# Host header from request not for us
|
||||
{:host_header, false} ->
|
||||
{:cancel, :host_header_mismatch}
|
||||
|
||||
# Origin / URL validation failed somewhere possibly due to spoofing
|
||||
{:error, :origin_containment_failed} ->
|
||||
{:cancel, :origin_containment_failed}
|
||||
|
|
@ -234,6 +250,7 @@ defmodule Pleroma.Workers.SignatureRetryWorker do
|
|||
defp log_signature_retry_rejection({:cancel, reason}, context)
|
||||
when reason in [
|
||||
:actor_signature_mismatch,
|
||||
:host_header_mismatch,
|
||||
:invalid_signature,
|
||||
:invalid_signature_retry_metadata,
|
||||
:missing_signature_retry_metadata,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue