5ed3235e2e32985c4d0b736326d185396160dc08
[akkoma] / lib / pleroma / web / plugs / http_signature_plug.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
6 import Plug.Conn
7 import Phoenix.Controller, only: [get_format: 1, text: 2]
8 alias Pleroma.Activity
9 alias Pleroma.Web.Router
10 alias Pleroma.Signature
11 alias Pleroma.Instances
12 require Logger
13
14 @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
15
16 def init(options) do
17 options
18 end
19
20 def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
21 conn
22 end
23
24 def call(conn, _opts) do
25 if get_format(conn) == "activity+json" do
26 conn
27 |> maybe_assign_valid_signature()
28 |> maybe_require_signature()
29 else
30 conn
31 end
32 end
33
34 def route_aliases(%{path_info: ["objects", id], query_string: query_string}) do
35 ap_id = Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :object, id)
36
37 with %Activity{} = activity <- Activity.get_by_object_ap_id_with_object(ap_id) do
38 ["/notice/#{activity.id}", "/notice/#{activity.id}?#{query_string}"]
39 else
40 _ -> []
41 end
42 end
43
44 def route_aliases(_), do: []
45
46 defp assign_valid_signature_on_route_aliases(conn, []), do: conn
47
48 defp assign_valid_signature_on_route_aliases(%{assigns: %{valid_signature: true}} = conn, _),
49 do: conn
50
51 defp assign_valid_signature_on_route_aliases(conn, [path | rest]) do
52 request_target = String.downcase("#{conn.method}") <> " #{path}"
53
54 conn =
55 conn
56 |> put_req_header("(request-target)", request_target)
57 |> case do
58 %{assigns: %{digest: digest}} = conn -> put_req_header(conn, "digest", digest)
59 conn -> conn
60 end
61
62 conn
63 |> assign(:valid_signature, HTTPSignatures.validate_conn(conn))
64 |> assign(:signature_actor_id, signature_host(conn))
65 |> assign_valid_signature_on_route_aliases(rest)
66 end
67
68 defp maybe_assign_valid_signature(conn) do
69 if has_signature_header?(conn) do
70 # set (request-target) header to the appropriate value
71 # we also replace the digest header with the one we computed
72 possible_paths =
73 route_aliases(conn) ++ [conn.request_path, conn.request_path <> "?#{conn.query_string}"]
74
75 assign_valid_signature_on_route_aliases(conn, possible_paths)
76 else
77 Logger.debug("No signature header!")
78 conn
79 end
80 end
81
82 defp has_signature_header?(conn) do
83 conn |> get_req_header("signature") |> Enum.at(0, false)
84 end
85
86 defp maybe_require_signature(
87 %{assigns: %{valid_signature: true, signature_actor_id: actor_id}} = conn
88 ) do
89 # inboxes implicitly need http signatures for authentication
90 # so we don't really know if the instance will have broken federation after
91 # we turn on authorized_fetch_mode.
92 #
93 # to "check" this is a signed fetch, verify if method is GET
94 if conn.method == "GET" do
95 actor_host = URI.parse(actor_id).host
96
97 case @cachex.get(:request_signatures_cache, actor_host) do
98 {:ok, nil} ->
99 Logger.debug("Successful signature from #{actor_host}")
100 Instances.set_request_signatures(actor_host)
101 @cachex.put(:request_signatures_cache, actor_host, true)
102
103 {:ok, true} ->
104 :noop
105
106 any ->
107 Logger.warn(
108 "expected request signature cache to return a boolean, instead got #{inspect(any)}"
109 )
110 end
111 end
112
113 conn
114 end
115
116 defp maybe_require_signature(%{assigns: %{valid_signature: true}} = conn), do: conn
117
118 defp maybe_require_signature(conn) do
119 if Pleroma.Config.get([:activitypub, :authorized_fetch_mode], false) do
120 conn
121 |> put_status(:unauthorized)
122 |> text("Request not signed")
123 |> halt()
124 else
125 conn
126 end
127 end
128
129 defp signature_host(conn) do
130 with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
131 {:ok, actor_id} <- Signature.key_id_to_actor_id(kid) do
132 actor_id
133 else
134 e ->
135 {:error, e}
136 end
137 end
138 end