Renew HTTP signatures when following redirects #973

Merged
Oneric merged 13 commits from Oneric/akkoma:httpsig_redirect_resign into develop 2025-10-04 16:25:50 +00:00
29 changed files with 252 additions and 510 deletions

View file

@ -47,6 +47,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- fixed handling of inlined "featured" collections
- fixed user endpoint serving invalid ActivityPub for minimal, authfetch-fallback responses
- remote emoji reacts from IceShrimp.NET instances are now handled consistently and always merged with identical other emoji reactions
- ActivityPub requests signatures are now renewed when following redirects making sure path and host actually match the final URL
### Changed
- Internal and relay actors are now again represented with type "Application"
@ -65,6 +66,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
instead of mapping them down to a boolean private/public
- we no longer repeatedly try to deliver to explicitly deleted inboxes
- outgoing requests may now use HTTP2 by default
- Config option `Pleroma.Web.MediaProxy.Invalidation.Http, :options` and
the `:http` subkey of `:media_proxy, :proxy_opts` now only accept
adapter-related settings inside the `:adapter` subkey, no longer on the top-level
## 2025.03

View file

@ -74,8 +74,6 @@ rum_enabled = System.get_env("RUM_ENABLED") == "true"
config :pleroma, :database, rum_enabled: rum_enabled
IO.puts("RUM enabled: #{rum_enabled}")
config :pleroma, Pleroma.ReverseProxy.Client, Pleroma.ReverseProxy.ClientMock
if File.exists?("./config/benchmark.secret.exs") do
import_config "benchmark.secret.exs"
else

View file

@ -580,7 +580,7 @@ defmodule Pleroma.Emoji.Pack do
defp http_get(%URI{} = url), do: url |> to_string() |> http_get()
defp http_get(url) do
with {:ok, %{body: body}} <- Pleroma.HTTP.get(url, [], []) do
with {:ok, %{body: body}} <- Pleroma.HTTP.get(url) do
Jason.decode(body)
end
end

View file

@ -84,7 +84,7 @@ defmodule Pleroma.Frontend do
url = String.replace(frontend_info["build_url"], "${ref}", frontend_info["ref"])
with {:ok, %{status: 200, body: zip_body}} <-
Pleroma.HTTP.get(url, [], receive_timeout: 120_000) do
Pleroma.HTTP.get(url, [], adapter: [receive_timeout: 120_000]) do
unzip(zip_body, dest)
else
{:error, e} -> {:error, e}

View file

@ -24,7 +24,7 @@ defmodule Pleroma.Helpers.MediaHelper do
def image_resize(url, options) do
with executable when is_binary(executable) <- System.find_executable("convert"),
{:ok, args} <- prepare_image_resize_args(options),
{:ok, env} <- HTTP.get(url, [], []),
{:ok, env} <- HTTP.get(url),
{:ok, fifo_path} <- mkfifo() do
args = List.flatten([fifo_path, args])
run_fifo(fifo_path, env, executable, args)
@ -73,7 +73,7 @@ defmodule Pleroma.Helpers.MediaHelper do
# Note: video thumbnail is intentionally not resized (always has original dimensions)
def video_framegrab(url) do
with executable when is_binary(executable) <- System.find_executable("ffmpeg"),
{:ok, env} <- HTTP.get(url, [], []),
{:ok, env} <- HTTP.get(url),
{:ok, fifo_path} <- mkfifo(),
args = [
"-y",

View file

@ -8,9 +8,6 @@ defmodule Pleroma.HTTP do
"""
alias Pleroma.HTTP.AdapterHelper
alias Pleroma.HTTP.Request
alias Pleroma.HTTP.RequestBuilder, as: Builder
alias Tesla.Client
alias Tesla.Env
require Logger
@ -18,6 +15,8 @@ defmodule Pleroma.HTTP do
@type t :: __MODULE__
@type method() :: :get | :post | :put | :delete | :head
@mix_env Mix.env()
@doc """
Performs GET request.
@ -60,7 +59,7 @@ defmodule Pleroma.HTTP do
{:ok, Env.t()} | {:error, any()}
def request(method, url, body, headers, options) when is_binary(url) do
uri = URI.parse(url)
adapter_opts = AdapterHelper.options(options || [])
adapter_opts = AdapterHelper.options(options[:adapter] || [])
adapter_opts =
if uri.scheme == :https do
@ -71,28 +70,43 @@ defmodule Pleroma.HTTP do
options = put_in(options[:adapter], adapter_opts)
params = options[:params] || []
request = build_request(method, headers, options, url, body, params)
client = Tesla.client([Tesla.Middleware.FollowRedirects, Tesla.Middleware.Telemetry])
options = options |> Keyword.delete(:params)
headers = maybe_add_user_agent(headers)
client =
Tesla.client([
Tesla.Middleware.FollowRedirects,
Pleroma.HTTP.Middleware.HTTPSignature,
Tesla.Middleware.Telemetry
])
Logger.debug("Outbound: #{method} #{url}")
request(client, request)
Tesla.request(client,
method: method,
url: url,
query: params,
headers: headers,
body: body,
opts: options
)
rescue
e ->
Logger.error("Failed to fetch #{url}: #{Exception.format(:error, e, __STACKTRACE__)}")
{:error, :fetch_error}
end
@spec request(Client.t(), keyword()) :: {:ok, Env.t()} | {:error, any()}
def request(client, request), do: Tesla.request(client, request)
defp build_request(method, headers, options, url, body, params) do
Builder.new()
|> Builder.method(method)
|> Builder.headers(headers)
|> Builder.opts(options)
|> Builder.url(url)
|> Builder.add_param(:body, :body, body)
|> Builder.add_param(:query, :query, params)
|> Builder.convert_to_keyword()
if @mix_env == :test do
defp maybe_add_user_agent(headers) do
with true <- Pleroma.Config.get([:http, :send_user_agent]) do
[{"user-agent", Pleroma.Application.user_agent()} | headers]
else
_ ->
headers
end
end
else
defp maybe_add_user_agent(headers),
do: [{"user-agent", Pleroma.Application.user_agent()} | headers]
end
end

View file

@ -0,0 +1,135 @@
# Akkoma: Magically expressive social media
# Copyright © 2025 Akkoma Authors <https://akkoma.dev/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.Middleware.HTTPSignature do
alias Pleroma.User.SigningKey
alias Pleroma.Signature
require Logger
@behaviour Tesla.Middleware
@moduledoc """
Adds a HTTP signature and related headers to requests, if a signing key is set in the request env.
If any other middleware can update the target location (e.g. redirects) this MUST be placed after all of them!
(Note: the third argument holds static middleware options from client creation)
"""
@doc """
If logging raw Tesla.Env use this if you wish to redact signing key details
"""
def redact_keys(env) do
case get_in(env, [:opts, :httpsig, :signing_key]) do
nil -> env
key -> put_in(env, [:opts, :httpsig, :signing_key], redact_key_details(key))
end
end
defp redact_key_details(%SigningKey{key_id: id}), do: id
defp redact_key_details(key), do: key
@impl true
def call(env, next, _options) do
env = maybe_sign(env)
Tesla.run(env, next)
end
defp maybe_sign(env) do
case Keyword.get(env.opts, :httpsig) do
%{signing_key: %SigningKey{} = key} ->
set_signature_headers(env, key)
_ ->
env
end
end
defp set_signature_headers(env, key) do
Logger.debug("Signing request to: #{env.url}")
{http_headers, signing_headers} = collect_headers_for_signature(env)
signature = Signature.sign(key, signing_headers, has_body: has_body(env))
set_headers(env, [{"signature", signature} | http_headers])
end
defp has_body(%{body: body}) when body in [nil, ""], do: false
defp has_body(_), do: true
defp set_headers(env, []), do: env
defp set_headers(env, [{key, val} | rest]) do
headers = :proplists.delete(key, env.headers)
headers = [{key, val} | headers]
set_headers(%{env | headers: headers}, rest)
end
# Returns tuple.
# First element is headers+values which need to be added to the HTTP request.
# Second element are all headers to be used for signing, including already existing and pseudo headers.
defp collect_headers_for_signature(env) do
{request_target, host} = get_request_target_and_host(env)
date = http_date()
# content-length is always automatically set later on
# since they are needed to establish working connection.
# Similarly host will always be set for HTTP/1, and technically may be omitted for HTTP/2+
# but Tesla doesnt handle it well if we preset it ourselves (and seems to set it even for HTTP/2 anyway)
http_headers = [{"date", date}]
signing_headers = %{
"(request-target)" => request_target,
"host" => host,
"date" => date
}
if has_body(env) do
append_body_headers(env, http_headers, signing_headers)
else
{http_headers, signing_headers}
end
end
defp append_body_headers(env, http_headers, signing_headers) do
content_length = byte_size(env.body)
digest = digest_value(env)
http_headers = [{"digest", digest} | http_headers]
signing_headers =
Map.merge(signing_headers, %{
"digest" => digest,
"content-length" => content_length
})
{http_headers, signing_headers}
end
defp get_request_target_and_host(env) do
uri = URI.parse(env.url)
rt = "#{env.method} #{uri.path}"
host = host_from_uri(uri)
{rt, host}
end
defp digest_value(env) do
# case Tesla.get_header(env, "digest")
encoded_hash = :crypto.hash(:sha256, env.body) |> Base.encode64()
"SHA-256=" <> encoded_hash
end
defp host_from_uri(%URI{port: port, scheme: scheme, host: host}) do
# https://httpwg.org/specs/rfc9110.html#field.host
# https://www.rfc-editor.org/rfc/rfc3986.html#section-3.2.3
if port == URI.default_port(scheme) do
host
else
"#{host}:#{port}"
end
end
defp http_date() do
now = NaiveDateTime.utc_now()
Timex.lformat!(now, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT", "en")
end
end

View file

@ -1,23 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.Request do
@moduledoc """
Request struct.
"""
defstruct method: :get, url: "", query: [], headers: [], body: "", opts: []
@type method :: :head | :get | :delete | :trace | :options | :post | :put | :patch
@type url :: String.t()
@type headers :: [{String.t(), String.t()}]
@type t :: %__MODULE__{
method: method(),
url: url(),
query: keyword(),
headers: headers(),
body: String.t(),
opts: keyword()
}
end

View file

@ -1,102 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.RequestBuilder do
@moduledoc """
Helper functions for building Tesla requests
"""
alias Pleroma.HTTP.Request
alias Tesla.Multipart
@mix_env Mix.env()
@doc """
Creates new request
"""
@spec new(Request.t()) :: Request.t()
def new(%Request{} = request \\ %Request{}), do: request
@doc """
Specify the request method when building a request
"""
@spec method(Request.t(), Request.method()) :: Request.t()
def method(request, m), do: %{request | method: m}
@doc """
Specify the request method when building a request
"""
@spec url(Request.t(), Request.url()) :: Request.t()
def url(request, u), do: %{request | url: u}
@doc """
Add headers to the request
"""
@spec headers(Request.t(), Request.headers()) :: Request.t()
def headers(request, headers) do
headers_list = maybe_add_user_agent(headers, @mix_env)
%{request | headers: headers_list}
end
@doc """
Add custom, per-request middleware or adapter options to the request
"""
@spec opts(Request.t(), keyword()) :: Request.t()
def opts(request, options), do: %{request | opts: options}
@doc """
Add optional parameters to the request
"""
@spec add_param(Request.t(), atom(), atom(), any()) :: Request.t()
def add_param(request, :query, :query, values), do: %{request | query: values}
def add_param(request, :body, :body, value), do: %{request | body: value}
def add_param(request, :body, key, value) do
request
|> Map.put(:body, Multipart.new())
|> Map.update!(
:body,
&Multipart.add_field(
&1,
key,
Jason.encode!(value),
headers: [{"content-type", "application/json"}]
)
)
end
def add_param(request, :file, name, path) do
request
|> Map.put(:body, Multipart.new())
|> Map.update!(:body, &Multipart.add_file(&1, path, name: name))
end
def add_param(request, :form, name, value) do
Map.update(request, :body, %{name => value}, &Map.put(&1, name, value))
end
def add_param(request, location, key, value) do
Map.update(request, location, [{key, value}], &(&1 ++ [{key, value}]))
end
def convert_to_keyword(request) do
request
|> Map.from_struct()
|> Enum.into([])
end
defp maybe_add_user_agent(headers, :test) do
with true <- Pleroma.Config.get([:http, :send_user_agent]) do
[{"user-agent", Pleroma.Application.user_agent()} | headers]
else
_ ->
headers
end
end
defp maybe_add_user_agent(headers, _),
do: [{"user-agent", Pleroma.Application.user_agent()} | headers]
end

View file

@ -10,15 +10,15 @@ defmodule Pleroma.HTTP.Tzdata do
alias Pleroma.HTTP
@impl true
def get(url, headers, options) do
with {:ok, %Tesla.Env{} = env} <- HTTP.get(url, headers, options) do
def get(url, headers, _options) do
with {:ok, %Tesla.Env{} = env} <- HTTP.get(url, headers) do
{:ok, {env.status, env.headers, env.body}}
end
end
@impl true
def head(url, headers, options) do
with {:ok, %Tesla.Env{} = env} <- HTTP.head(url, headers, options) do
def head(url, headers, _options) do
with {:ok, %Tesla.Env{} = env} <- HTTP.head(url, headers) do
{:ok, {env.status, env.headers}}
end
end

View file

@ -5,8 +5,8 @@
defmodule Pleroma.HTTP.WebPush do
@moduledoc false
def post(url, payload, headers, options \\ []) do
def post(url, payload, headers, _options) do
list_headers = Map.to_list(headers)
Pleroma.HTTP.post(url, payload, list_headers, options)
Pleroma.HTTP.post(url, payload, list_headers)
end
end

View file

@ -242,7 +242,7 @@ defmodule Pleroma.Instances.Instance do
{:ok,
Enum.find(links, &(&1["rel"] == "http://nodeinfo.diaspora.software/ns/schema/2.0"))},
{:ok, %Tesla.Env{body: data}} <-
Pleroma.HTTP.get(href, [{"accept", "application/json"}], []),
Pleroma.HTTP.get(href, [{"accept", "application/json"}]),
{:length, true} <- {:length, String.length(data) < 50_000},
{:ok, nodeinfo} <- Jason.decode(data) do
nodeinfo
@ -270,7 +270,7 @@ defmodule Pleroma.Instances.Instance do
with true <- Pleroma.Config.get([:instances_favicons, :enabled]),
{_, true} <- {:reachable, reachable?(instance_uri.host)},
{:ok, %Tesla.Env{body: html}} <-
Pleroma.HTTP.get(to_string(instance_uri), [{"accept", "text/html"}], []),
Pleroma.HTTP.get(to_string(instance_uri), [{"accept", "text/html"}]),
{_, [favicon_rel | _]} when is_binary(favicon_rel) <-
{:parse, html |> Floki.parse_document!() |> Floki.attribute("link[rel=icon]", "href")},
{_, favicon} when is_binary(favicon) <-

View file

@ -9,7 +9,6 @@ defmodule Pleroma.Object.Fetcher do
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.Repo
alias Pleroma.Signature
alias Pleroma.Web.ActivityPub.InternalFetchActor
alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.Transmogrifier
@ -227,36 +226,6 @@ defmodule Pleroma.Object.Fetcher do
|> Maps.put_if_present("bcc", data["bcc"])
end
defp make_signature(id, date) do
uri = URI.parse(id)
signature =
InternalFetchActor.get_actor()
|> Signature.sign(%{
"(request-target)" => "get #{uri.path}",
"host" => uri.host,
"date" => date
})
{"signature", signature}
end
defp sign_fetch(headers, id, date) do
if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do
[make_signature(id, date) | headers]
else
headers
end
end
defp maybe_date_fetch(headers, date) do
if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do
[{"date", date} | headers]
else
headers
end
end
@doc """
Fetches arbitrary remote object and performs basic safety and authenticity checks.
When the fetch URL is known to already be a canonical AP id, checks are stricter.
@ -402,20 +371,25 @@ defmodule Pleroma.Object.Fetcher do
@doc "Do NOT use; only public for use in tests"
def get_object(id) do
date = Pleroma.Signature.signed_date()
headers =
[
# The first is required by spec, the second provided as a fallback for buggy implementations
{"accept", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""},
{"accept", "application/activity+json"}
]
|> maybe_date_fetch(date)
|> sign_fetch(id, date)
http_opts =
if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do
signing_actor = InternalFetchActor.get_actor() |> Pleroma.User.SigningKey.load_key()
signing_key = signing_actor.signing_key
[httpsig: %{signing_key: signing_key}]
else
[]
end
with {:ok, %{body: body, status: code, headers: headers, url: final_url}}
when code in 200..299 <-
HTTP.Backoff.get(id, headers),
HTTP.Backoff.get(id, headers, http_opts),
{:has_content_type, {_, content_type}} <-
{:has_content_type, List.keyfind(headers, "content-type", 0)},
{:parse_content_type, {:ok, "application", subtype, type_params}} <-
@ -443,6 +417,13 @@ defmodule Pleroma.Object.Fetcher do
{:ok, %{status: code}} when code in [404, 410] ->
{:error, :not_found}
{:ok, %{status: code, headers: headers}} ->
{:error, {:http_error, code, headers}}
# connection/protocol-related error
{:ok, %Tesla.Env{} = env} ->
{:error, {:http_error, :connect, Pleroma.HTTP.Middleware.HTTPSignature.redact_keys(env)}}
{:error, e} ->
{:error, e}

View file

@ -1,20 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReverseProxy.Client do
@type status :: pos_integer()
@type header_name :: String.t()
@type header_value :: String.t()
@type headers :: [{header_name(), header_value()}]
@callback request(atom(), String.t(), headers(), String.t(), list()) ::
{:ok, status(), headers(), reference() | map()}
| {:ok, status(), headers()}
| {:ok, reference()}
| {:error, term()}
@callback stream_body(map()) :: {:ok, binary(), map()} | :done | {:error, atom() | String.t()}
@callback close(reference() | pid() | map()) :: :ok
end

View file

@ -1,77 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReverseProxy.Client.Tesla do
@behaviour Pleroma.ReverseProxy.Client
@type headers() :: [{String.t(), String.t()}]
@type status() :: pos_integer()
@spec request(atom(), String.t(), headers(), String.t(), keyword()) ::
{:ok, status(), headers}
| {:ok, status(), headers, map()}
| {:error, atom() | String.t()}
| no_return()
@impl true
def request(method, url, headers, body, opts \\ []) do
check_adapter()
opts = Keyword.put(opts, :body_as, :chunks)
with {:ok, response} <-
Pleroma.HTTP.request(
method,
url,
body,
headers,
opts
) do
if is_map(response.body) and method != :head do
{:ok, response.status, response.headers, response.body}
else
{:ok, response.status, response.headers}
end
else
{:error, error} -> {:error, error}
end
end
@impl true
@spec stream_body(map()) ::
{:ok, binary(), map()} | {:error, atom() | String.t()} | :done | no_return()
def stream_body(%{pid: _pid, fin: true}) do
:done
end
def stream_body(client) do
case read_chunk!(client) do
{:fin, body} ->
{:ok, body, Map.put(client, :fin, true)}
{:nofin, part} ->
{:ok, part, client}
{:error, error} ->
{:error, error}
end
end
defp read_chunk!(%{pid: pid, stream: stream, opts: opts}) do
adapter = check_adapter()
adapter.read_chunk(pid, stream, opts)
end
@impl true
@spec close(map) :: :ok | no_return()
def close(%{pid: _pid}) do
:ok
end
defp check_adapter do
adapter = Application.get_env(:tesla, :adapter)
adapter
end
end

View file

@ -1,28 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReverseProxy.Client.Wrapper do
@moduledoc "Meta-client that calls the appropriate client from the config."
@behaviour Pleroma.ReverseProxy.Client
@impl true
def request(method, url, headers, body \\ "", opts \\ []) do
client().request(method, url, headers, body, opts)
end
@impl true
def stream_body(ref), do: client().stream_body(ref)
@impl true
def close(ref), do: client().close(ref)
defp client do
:tesla
|> Application.get_env(:adapter)
|> client()
end
defp client({Tesla.Adapter.Finch, _}), do: Pleroma.ReverseProxy.Client.Tesla
defp client(_), do: Pleroma.Config.get!(Pleroma.ReverseProxy.Client)
end

View file

@ -62,20 +62,16 @@ defmodule Pleroma.Signature do
end
end
def sign(%User{} = user, headers, opts \\ []) do
with {:ok, private_key} <- SigningKey.private_key(user) do
def sign(%SigningKey{} = key, headers, opts \\ []) do
with {:ok, private_key_binary} <- SigningKey.private_key_binary(key) do
HTTPSignatures.sign(
%HTTPKey{key: private_key},
SigningKey.local_key_id(user.ap_id),
%HTTPKey{key: private_key_binary},
key.key_id,
headers,
opts
)
else
_ -> raise "Tried to sign with #{key.key_id} but it has no private key!"
end
end
def signed_date, do: signed_date(NaiveDateTime.utc_now())
def signed_date(%NaiveDateTime{} = date) do
Timex.lformat!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT", "en")
end
end

View file

@ -136,24 +136,22 @@ defmodule Pleroma.User.SigningKey do
{:error, "key not found"}
end
@spec private_key(User.t()) :: {:ok, binary()} | {:error, String.t()}
@spec private_key_binary(__MODULE__) :: {:ok, binary()} | {:error, String.t()}
@doc """
Given a user, return the private key for that user in binary format.
Given a key, return the corresponding private key in binary format.
"""
def private_key(%User{} = user) do
case Repo.preload(user, :signing_key) do
%{signing_key: %__MODULE__{private_key: private_key_pem}} ->
key =
private_key_pem
|> :public_key.pem_decode()
|> hd()
|> :public_key.pem_entry_decode()
def private_key_binary(%__MODULE__{private_key: private_key_pem}) do
key =
private_key_pem
|> :public_key.pem_decode()
|> hd()
|> :public_key.pem_entry_decode()
{:ok, key}
{:ok, key}
end
_ ->
{:error, "key not found"}
end
def private_key_binary(%__MODULE__{} = key) do
{:error, "key #{key.key_id} has no private key"}
end
@spec get_or_fetch_by_key_id(String.t()) :: {:ok, __MODULE__} | {:error, String.t()}

View file

@ -11,8 +11,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
require Logger
@adapter_options [
receive_timeout: 10_000
@http_options [
adapter: [receive_timeout: 10_000]
]
@impl true
@ -36,7 +36,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
end
end
defp fetch(url), do: HTTP.get(url, [], @adapter_options)
defp fetch(url), do: HTTP.get(url, [], @http_options)
defp preload(%{"object" => %{"attachment" => attachments}} = _message) do
Enum.each(attachments, fn

View file

@ -112,7 +112,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
defp is_remote_size_within_limit?(url) do
with {:ok, %{status: status, headers: headers} = _response} when status in 200..299 <-
Pleroma.HTTP.request(:head, url, nil, [], []) do
Pleroma.HTTP.head(url) do
content_length = get_int_header(headers, "content-length")
size_limit = Config.get([:mrf_steal_emoji, :size_limit], @size_limit)

View file

@ -51,34 +51,15 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
%{"inbox" => inbox, "json" => json, "actor" => %User{} = actor, "id" => id} = params
) do
Logger.debug("Federating #{id} to #{inbox}")
uri = %{path: path} = URI.parse(inbox)
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
date = Pleroma.Signature.signed_date()
signature =
Pleroma.Signature.sign(
actor,
%{
"(request-target)" => "post #{path}",
"host" => signature_host(uri),
"content-length" => byte_size(json),
"digest" => digest,
"date" => date
},
has_body: true
)
signing_key = Pleroma.User.SigningKey.load_key(actor).signing_key
with {:ok, %{status: code}} = result when code in 200..299 <-
HTTP.post(
inbox,
json,
[
{"Content-Type", "application/activity+json"},
{"Date", date},
{"signature", signature},
{"digest", digest}
]
[{"content-type", "application/activity+json"}],
httpsig: %{signing_key: signing_key}
) do
if not Map.has_key?(params, "unreachable_since") || params["unreachable_since"] do
Instances.set_reachable(inbox)
@ -88,7 +69,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
else
{_post_result, response} ->
unless params["unreachable_since"], do: Instances.set_unreachable(inbox)
{:error, response}
{:error, format_error_response(response)}
end
end
@ -101,13 +82,13 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|> publish_one()
end
defp signature_host(%URI{port: port, scheme: scheme, host: host}) do
if port == URI.default_port(scheme) do
host
else
"#{host}:#{port}"
end
end
defp format_error_response(%Tesla.Env{status: code, headers: headers}),
do: {:http_error, code, headers}
defp format_error_response(%Tesla.Env{} = env),
do: {:http_error, :connect, Pleroma.HTTP.Middleware.HTTPSignature.redact_keys(env)}
defp format_error_response(response), do: response
defp blocked_instances do
Config.get([:instance, :quarantined_instances], []) ++

View file

@ -54,7 +54,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do
media_proxy_url = MediaProxy.url(url)
with {:ok, %{status: status} = head_response} when status in 200..299 <-
Pleroma.HTTP.request("HEAD", media_proxy_url, [], [], name: MyFinch) do
Pleroma.HTTP.head(media_proxy_url) do
content_type = Tesla.get_header(head_response, "content-type")
content_length = Tesla.get_header(head_response, "content-length")
content_length = content_length && String.to_integer(content_length)

View file

@ -4,8 +4,7 @@
defmodule Pleroma.Web.RelMe do
@options [
max_body: 2_000_000,
receive_timeout: 2_000
adapter: [receive_timeout: 2_000]
]
if Pleroma.Config.get(:env) == :test do

View file

@ -9,7 +9,7 @@ defmodule Pleroma.Web.RichMedia.Helpers do
headers = [{"user-agent", Pleroma.Application.user_agent() <> "; Bot"}]
head_check =
case Pleroma.HTTP.head(url, headers, http_options()) do
case Pleroma.HTTP.head(url, headers) do
# If the HEAD request didn't reach the server for whatever reason,
# we assume the GET that comes right after won't either
{:error, _} = e ->
@ -24,7 +24,7 @@ defmodule Pleroma.Web.RichMedia.Helpers do
:ok
end
with :ok <- head_check, do: Pleroma.HTTP.get(url, headers, http_options())
with :ok <- head_check, do: Pleroma.HTTP.get(url, headers)
end
defp check_content_type(headers) do
@ -41,7 +41,7 @@ defmodule Pleroma.Web.RichMedia.Helpers do
end
defp check_content_length(headers) do
max_body = Keyword.get(http_options(), :max_body)
max_body = Config.get([:rich_media, :max_body], 5_000_000)
case List.keyfind(headers, "content-length", 0) do
{_, maybe_content_length} ->
@ -55,11 +55,4 @@ defmodule Pleroma.Web.RichMedia.Helpers do
:ok
end
end
defp http_options do
[
pool: :media,
max_body: Config.get([:rich_media, :max_body], 5_000_000)
]
end
end

View file

@ -40,7 +40,7 @@ defmodule Pleroma.Workers.PublisherWorker do
# instance / actor was explicitly deleted; theres nothing to deliver to anymore
# since we dont know whether the whole instance is gone or just this actor,
# do NOT immediately mark the instance as unreachable
{:error, %{status: 410}} ->
{:error, {:http_error, 410, _}} ->
:ok
res ->

View file

@ -1,93 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.RequestBuilderTest do
use ExUnit.Case
use Pleroma.Tests.Helpers
alias Pleroma.HTTP.Request
alias Pleroma.HTTP.RequestBuilder
describe "headers/2" do
test "don't send pleroma user agent" do
assert RequestBuilder.headers(%Request{}, []) == %Request{headers: []}
end
test "send pleroma user agent" do
clear_config([:http, :send_user_agent], true)
clear_config([:http, :user_agent], :default)
assert RequestBuilder.headers(%Request{}, []) == %Request{
headers: [{"user-agent", Pleroma.Application.user_agent()}]
}
end
test "send custom user agent" do
clear_config([:http, :send_user_agent], true)
clear_config([:http, :user_agent], "totally-not-pleroma")
assert RequestBuilder.headers(%Request{}, []) == %Request{
headers: [{"user-agent", "totally-not-pleroma"}]
}
end
end
describe "add_param/4" do
test "add file parameter" do
assert match?(
%Request{
body: %Tesla.Multipart{
boundary: _,
content_type_params: [],
parts: [
%Tesla.Multipart.Part{
body: %File.Stream{
line_or_bytes: 2048,
modes: [:raw, :read_ahead, :binary],
path: "some-path/filename.png",
raw: true
},
dispositions: [name: "filename.png", filename: "filename.png"],
headers: []
}
]
}
},
RequestBuilder.add_param(
%Request{},
:file,
"filename.png",
"some-path/filename.png"
)
)
end
test "add key to body" do
%{
body: %Tesla.Multipart{
boundary: _,
content_type_params: [],
parts: [
%Tesla.Multipart.Part{
body: "\"someval\"",
dispositions: [name: "somekey"],
headers: [{"content-type", "application/json"}]
}
]
}
} = RequestBuilder.add_param(%{}, :body, "somekey", "someval")
end
test "add form parameter" do
assert RequestBuilder.add_param(%{}, :form, "somename", "someval") == %{
body: %{"somename" => "someval"}
}
end
test "add for location" do
assert RequestBuilder.add_param(%{}, :some_location, "somekey", "someval") == %{
some_location: [{"somekey", "someval"}]
}
end
end
end

View file

@ -478,7 +478,7 @@ defmodule Pleroma.Object.FetcherTest do
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
assert called(Pleroma.Signature.sign(:_, :_))
assert called(Pleroma.Signature.sign(:_, :_, :_))
end
test_with_mock "it doesn't sign fetches when not configured to do so",
@ -489,7 +489,7 @@ defmodule Pleroma.Object.FetcherTest do
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
refute called(Pleroma.Signature.sign(:_, :_))
refute called(Pleroma.Signature.sign(:_, :_, :_))
end
end

View file

@ -8,7 +8,6 @@ defmodule Pleroma.SignatureTest do
import Pleroma.Factory
import Tesla.Mock
import Mock
alias HTTPSignatures.HTTPKey
alias Pleroma.Signature
@ -130,24 +129,11 @@ defmodule Pleroma.SignatureTest do
assert_signature_equal(
Signature.sign(
user,
user.signing_key,
headers
),
~s|keyId="https://mastodon.social/users/lambadalambda#main-key",algorithm="rsa-sha256",headers="(request-target) content-length date digest host",signature="fhOT6IBThnCo6rv2Tv8BRXLV7LvVf/7wTX/bbPLtdq5A4GUqrmXUcY5p77jQ6NU9IRIVczeeStxQV6TrHqk/qPdqQOzDcB6cWsSfrB1gsTinBbAWdPzQYqUOTl+Minqn2RERAfPebKYr9QGa0sTODDHvze/UFPuL8a1lDO2VQE0lRCdg49Igr8pGl/CupUx8Fb874omqP0ba3M+siuKEwo02m9hHcbZUeLSN0ZVdvyTMttyqPM1BfwnFXkaQRAblLTyzt4Fv2+fTN+zPipSxJl1YIo1TsmwNq9klqImpjh8NHM3MJ5eZxTZ109S6Q910n1Lm46V/SqByDaYeg9g7Jw=="|
)
end
end
describe "signed_date" do
test "it returns formatted current date" do
with_mock(NaiveDateTime, utc_now: fn -> ~N[2019-08-23 18:11:24.822233] end) do
assert Signature.signed_date() == "Fri, 23 Aug 2019 18:11:24 GMT"
end
end
test "it returns formatted date" do
assert Signature.signed_date(~N[2019-08-23 08:11:24.822233]) ==
"Fri, 23 Aug 2019 08:11:24 GMT"
end
end
end

View file

@ -163,7 +163,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
media_proxy_url: media_proxy_url
} do
Tesla.Mock.mock(fn
%{method: "HEAD", url: ^media_proxy_url} ->
%{method: :head, url: ^media_proxy_url} ->
%Tesla.Env{status: 500, body: ""}
end)
@ -178,7 +178,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
media_proxy_url: media_proxy_url
} do
Tesla.Mock.mock(fn
%{method: "HEAD", url: ^media_proxy_url} ->
%{method: :head, url: ^media_proxy_url} ->
%Tesla.Env{status: 200, body: "", headers: [{"content-type", "application/pdf"}]}
end)
@ -198,7 +198,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
clear_config([:media_preview_proxy, :min_content_length], 1_000_000_000)
Tesla.Mock.mock(fn
%{method: "HEAD", url: ^media_proxy_url} ->
%{method: :head, url: ^media_proxy_url} ->
%Tesla.Env{
status: 200,
body: "",
@ -223,7 +223,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
media_proxy_url: media_proxy_url
} do
Tesla.Mock.mock(fn
%{method: "HEAD", url: ^media_proxy_url} ->
%{method: :head, url: ^media_proxy_url} ->
%Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/gif"}]}
end)
@ -241,7 +241,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
media_proxy_url: media_proxy_url
} do
Tesla.Mock.mock(fn
%{method: "HEAD", url: ^media_proxy_url} ->
%{method: :head, url: ^media_proxy_url} ->
%Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]}
end)
@ -261,7 +261,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
clear_config([:media_preview_proxy, :min_content_length], 100_000)
Tesla.Mock.mock(fn
%{method: "HEAD", url: ^media_proxy_url} ->
%{method: :head, url: ^media_proxy_url} ->
%Tesla.Env{
status: 200,
body: "",
@ -283,7 +283,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
assert_dependencies_installed()
Tesla.Mock.mock(fn
%{method: "HEAD", url: ^media_proxy_url} ->
%{method: :head, url: ^media_proxy_url} ->
%Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/png"}]}
%{method: :get, url: ^media_proxy_url} ->
@ -305,7 +305,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
assert_dependencies_installed()
Tesla.Mock.mock(fn
%{method: "HEAD", url: ^media_proxy_url} ->
%{method: :head, url: ^media_proxy_url} ->
%Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]}
%{method: :get, url: ^media_proxy_url} ->
@ -325,7 +325,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
media_proxy_url: media_proxy_url
} do
Tesla.Mock.mock(fn
%{method: "HEAD", url: ^media_proxy_url} ->
%{method: :head, url: ^media_proxy_url} ->
%Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]}
%{method: :get, url: ^media_proxy_url} ->