summaryrefslogtreecommitdiff
path: root/lib/pleroma/web/plugs
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pleroma/web/plugs')
-rw-r--r--lib/pleroma/web/plugs/admin_secret_authentication_plug.ex60
-rw-r--r--lib/pleroma/web/plugs/authentication_plug.ex79
-rw-r--r--lib/pleroma/web/plugs/basic_auth_decoder_plug.ex25
-rw-r--r--lib/pleroma/web/plugs/cache.ex136
-rw-r--r--lib/pleroma/web/plugs/digest_plug.ex14
-rw-r--r--lib/pleroma/web/plugs/ensure_authenticated_plug.ex41
-rw-r--r--lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex35
-rw-r--r--lib/pleroma/web/plugs/ensure_user_key_plug.ex18
-rw-r--r--lib/pleroma/web/plugs/expect_authenticated_check_plug.ex20
-rw-r--r--lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex21
-rw-r--r--lib/pleroma/web/plugs/federating_plug.ex32
-rw-r--r--lib/pleroma/web/plugs/frontend_static.ex59
-rw-r--r--lib/pleroma/web/plugs/http_security_plug.ex225
-rw-r--r--lib/pleroma/web/plugs/http_signature_plug.ex65
-rw-r--r--lib/pleroma/web/plugs/idempotency_plug.ex84
-rw-r--r--lib/pleroma/web/plugs/instance_static.ex53
-rw-r--r--lib/pleroma/web/plugs/legacy_authentication_plug.ex41
-rw-r--r--lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex71
-rw-r--r--lib/pleroma/web/plugs/o_auth_plug.ex120
-rw-r--r--lib/pleroma/web/plugs/o_auth_scopes_plug.ex77
-rw-r--r--lib/pleroma/web/plugs/plug_helper.ex40
-rw-r--r--lib/pleroma/web/plugs/rate_limiter.ex267
-rw-r--r--lib/pleroma/web/plugs/rate_limiter/limiter_supervisor.ex54
-rw-r--r--lib/pleroma/web/plugs/rate_limiter/supervisor.ex20
-rw-r--r--lib/pleroma/web/plugs/remote_ip.ex48
-rw-r--r--lib/pleroma/web/plugs/session_authentication_plug.ex21
-rw-r--r--lib/pleroma/web/plugs/set_format_plug.ex24
-rw-r--r--lib/pleroma/web/plugs/set_locale_plug.ex63
-rw-r--r--lib/pleroma/web/plugs/set_user_session_id_plug.ex19
-rw-r--r--lib/pleroma/web/plugs/static_fe_plug.ex26
-rw-r--r--lib/pleroma/web/plugs/trailing_format_plug.ex42
-rw-r--r--lib/pleroma/web/plugs/uploaded_media.ex107
-rw-r--r--lib/pleroma/web/plugs/user_enabled_plug.ex23
-rw-r--r--lib/pleroma/web/plugs/user_fetcher_plug.ex21
-rw-r--r--lib/pleroma/web/plugs/user_is_admin_plug.ex24
35 files changed, 2075 insertions, 0 deletions
diff --git a/lib/pleroma/web/plugs/admin_secret_authentication_plug.ex b/lib/pleroma/web/plugs/admin_secret_authentication_plug.ex
new file mode 100644
index 000000000..d7d4e4092
--- /dev/null
+++ b/lib/pleroma/web/plugs/admin_secret_authentication_plug.ex
@@ -0,0 +1,60 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.AdminSecretAuthenticationPlug do
+ import Plug.Conn
+
+ alias Pleroma.User
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.Plugs.RateLimiter
+
+ def init(options) do
+ options
+ end
+
+ def secret_token do
+ case Pleroma.Config.get(:admin_token) do
+ blank when blank in [nil, ""] -> nil
+ token -> token
+ end
+ end
+
+ def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
+
+ def call(conn, _) do
+ if secret_token() do
+ authenticate(conn)
+ else
+ conn
+ end
+ end
+
+ def authenticate(%{params: %{"admin_token" => admin_token}} = conn) do
+ if admin_token == secret_token() do
+ assign_admin_user(conn)
+ else
+ handle_bad_token(conn)
+ end
+ end
+
+ def authenticate(conn) do
+ token = secret_token()
+
+ case get_req_header(conn, "x-admin-token") do
+ blank when blank in [[], [""]] -> conn
+ [^token] -> assign_admin_user(conn)
+ _ -> handle_bad_token(conn)
+ end
+ end
+
+ defp assign_admin_user(conn) do
+ conn
+ |> assign(:user, %User{is_admin: true})
+ |> OAuthScopesPlug.skip_plug()
+ end
+
+ defp handle_bad_token(conn) do
+ RateLimiter.call(conn, name: :authentication)
+ end
+end
diff --git a/lib/pleroma/web/plugs/authentication_plug.ex b/lib/pleroma/web/plugs/authentication_plug.ex
new file mode 100644
index 000000000..e2a8b1b69
--- /dev/null
+++ b/lib/pleroma/web/plugs/authentication_plug.ex
@@ -0,0 +1,79 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.AuthenticationPlug do
+ alias Pleroma.User
+
+ import Plug.Conn
+
+ require Logger
+
+ def init(options), do: options
+
+ def checkpw(password, "$6" <> _ = password_hash) do
+ :crypt.crypt(password, password_hash) == password_hash
+ end
+
+ def checkpw(password, "$2" <> _ = password_hash) do
+ # Handle bcrypt passwords for Mastodon migration
+ Bcrypt.verify_pass(password, password_hash)
+ end
+
+ def checkpw(password, "$pbkdf2" <> _ = password_hash) do
+ Pbkdf2.verify_pass(password, password_hash)
+ end
+
+ def checkpw(_password, _password_hash) do
+ Logger.error("Password hash not recognized")
+ false
+ end
+
+ def maybe_update_password(%User{password_hash: "$2" <> _} = user, password) do
+ do_update_password(user, password)
+ end
+
+ def maybe_update_password(%User{password_hash: "$6" <> _} = user, password) do
+ do_update_password(user, password)
+ end
+
+ def maybe_update_password(user, _), do: {:ok, user}
+
+ defp do_update_password(user, password) do
+ user
+ |> User.password_update_changeset(%{
+ "password" => password,
+ "password_confirmation" => password
+ })
+ |> Pleroma.Repo.update()
+ end
+
+ def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
+
+ def call(
+ %{
+ assigns: %{
+ auth_user: %{password_hash: password_hash} = auth_user,
+ auth_credentials: %{password: password}
+ }
+ } = conn,
+ _
+ ) do
+ if checkpw(password, password_hash) do
+ {:ok, auth_user} = maybe_update_password(auth_user, password)
+
+ conn
+ |> assign(:user, auth_user)
+ |> Pleroma.Web.Plugs.OAuthScopesPlug.skip_plug()
+ else
+ conn
+ end
+ end
+
+ def call(%{assigns: %{auth_credentials: %{password: _}}} = conn, _) do
+ Pbkdf2.no_user_verify()
+ conn
+ end
+
+ def call(conn, _), do: conn
+end
diff --git a/lib/pleroma/web/plugs/basic_auth_decoder_plug.ex b/lib/pleroma/web/plugs/basic_auth_decoder_plug.ex
new file mode 100644
index 000000000..4dadfb000
--- /dev/null
+++ b/lib/pleroma/web/plugs/basic_auth_decoder_plug.ex
@@ -0,0 +1,25 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.BasicAuthDecoderPlug do
+ import Plug.Conn
+
+ def init(options) do
+ options
+ end
+
+ def call(conn, _opts) do
+ with ["Basic " <> header] <- get_req_header(conn, "authorization"),
+ {:ok, userinfo} <- Base.decode64(header),
+ [username, password] <- String.split(userinfo, ":", parts: 2) do
+ conn
+ |> assign(:auth_credentials, %{
+ username: username,
+ password: password
+ })
+ else
+ _ -> conn
+ end
+ end
+end
diff --git a/lib/pleroma/web/plugs/cache.ex b/lib/pleroma/web/plugs/cache.ex
new file mode 100644
index 000000000..6de01804a
--- /dev/null
+++ b/lib/pleroma/web/plugs/cache.ex
@@ -0,0 +1,136 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.Cache do
+ @moduledoc """
+ Caches successful GET responses.
+
+ To enable the cache add the plug to a router pipeline or controller:
+
+ plug(Pleroma.Web.Plugs.Cache)
+
+ ## Configuration
+
+ To configure the plug you need to pass settings as the second argument to the `plug/2` macro:
+
+ plug(Pleroma.Web.Plugs.Cache, [ttl: nil, query_params: true])
+
+ Available options:
+
+ - `ttl`: An expiration time (time-to-live). This value should be in milliseconds or `nil` to disable expiration. Defaults to `nil`.
+ - `query_params`: Take URL query string into account (`true`), ignore it (`false`) or limit to specific params only (list). Defaults to `true`.
+ - `tracking_fun`: A function that is called on successfull responses, no matter if the request is cached or not. It should accept a conn as the first argument and the value assigned to `tracking_fun_data` as the second.
+
+ Additionally, you can overwrite the TTL inside a controller action by assigning `cache_ttl` to the connection struct:
+
+ def index(conn, _params) do
+ ttl = 60_000 # one minute
+
+ conn
+ |> assign(:cache_ttl, ttl)
+ |> render("index.html")
+ end
+
+ """
+
+ import Phoenix.Controller, only: [current_path: 1, json: 2]
+ import Plug.Conn
+
+ @behaviour Plug
+
+ @defaults %{ttl: nil, query_params: true}
+
+ @impl true
+ def init([]), do: @defaults
+
+ def init(opts) do
+ opts = Map.new(opts)
+ Map.merge(@defaults, opts)
+ end
+
+ @impl true
+ def call(%{method: "GET"} = conn, opts) do
+ key = cache_key(conn, opts)
+
+ case Cachex.get(:web_resp_cache, key) do
+ {:ok, nil} ->
+ cache_resp(conn, opts)
+
+ {:ok, {content_type, body, tracking_fun_data}} ->
+ conn = opts.tracking_fun.(conn, tracking_fun_data)
+
+ send_cached(conn, {content_type, body})
+
+ {:ok, record} ->
+ send_cached(conn, record)
+
+ {atom, message} when atom in [:ignore, :error] ->
+ render_error(conn, message)
+ end
+ end
+
+ def call(conn, _), do: conn
+
+ # full path including query params
+ defp cache_key(conn, %{query_params: true}), do: current_path(conn)
+
+ # request path without query params
+ defp cache_key(conn, %{query_params: false}), do: conn.request_path
+
+ # request path with specific query params
+ defp cache_key(conn, %{query_params: query_params}) when is_list(query_params) do
+ query_string =
+ conn.params
+ |> Map.take(query_params)
+ |> URI.encode_query()
+
+ conn.request_path <> "?" <> query_string
+ end
+
+ defp cache_resp(conn, opts) do
+ register_before_send(conn, fn
+ %{status: 200, resp_body: body} = conn ->
+ ttl = Map.get(conn.assigns, :cache_ttl, opts.ttl)
+ key = cache_key(conn, opts)
+ content_type = content_type(conn)
+
+ conn =
+ unless opts[:tracking_fun] do
+ Cachex.put(:web_resp_cache, key, {content_type, body}, ttl: ttl)
+ conn
+ else
+ tracking_fun_data = Map.get(conn.assigns, :tracking_fun_data, nil)
+ Cachex.put(:web_resp_cache, key, {content_type, body, tracking_fun_data}, ttl: ttl)
+
+ opts.tracking_fun.(conn, tracking_fun_data)
+ end
+
+ put_resp_header(conn, "x-cache", "MISS from Pleroma")
+
+ conn ->
+ conn
+ end)
+ end
+
+ defp content_type(conn) do
+ conn
+ |> Plug.Conn.get_resp_header("content-type")
+ |> hd()
+ end
+
+ defp send_cached(conn, {content_type, body}) do
+ conn
+ |> put_resp_content_type(content_type, nil)
+ |> put_resp_header("x-cache", "HIT from Pleroma")
+ |> send_resp(:ok, body)
+ |> halt()
+ end
+
+ defp render_error(conn, message) do
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: message})
+ |> halt()
+ end
+end
diff --git a/lib/pleroma/web/plugs/digest_plug.ex b/lib/pleroma/web/plugs/digest_plug.ex
new file mode 100644
index 000000000..b521b3073
--- /dev/null
+++ b/lib/pleroma/web/plugs/digest_plug.ex
@@ -0,0 +1,14 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.DigestPlug do
+ alias Plug.Conn
+ require Logger
+
+ def read_body(conn, opts) do
+ {:ok, body, conn} = Conn.read_body(conn, opts)
+ digest = "SHA-256=" <> (:crypto.hash(:sha256, body) |> Base.encode64())
+ {:ok, body, Conn.assign(conn, :digest, digest)}
+ end
+end
diff --git a/lib/pleroma/web/plugs/ensure_authenticated_plug.ex b/lib/pleroma/web/plugs/ensure_authenticated_plug.ex
new file mode 100644
index 000000000..ea2af6881
--- /dev/null
+++ b/lib/pleroma/web/plugs/ensure_authenticated_plug.ex
@@ -0,0 +1,41 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.EnsureAuthenticatedPlug do
+ import Plug.Conn
+ import Pleroma.Web.TranslationHelpers
+
+ alias Pleroma.User
+
+ use Pleroma.Web, :plug
+
+ def init(options) do
+ options
+ end
+
+ @impl true
+ def perform(
+ %{
+ assigns: %{
+ auth_credentials: %{password: _},
+ user: %User{multi_factor_authentication_settings: %{enabled: true}}
+ }
+ } = conn,
+ _
+ ) do
+ conn
+ |> render_error(:forbidden, "Two-factor authentication enabled, you must use a access token.")
+ |> halt()
+ end
+
+ def perform(%{assigns: %{user: %User{}}} = conn, _) do
+ conn
+ end
+
+ def perform(conn, _) do
+ conn
+ |> render_error(:forbidden, "Invalid credentials.")
+ |> halt()
+ end
+end
diff --git a/lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex b/lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex
new file mode 100644
index 000000000..3bebdac6d
--- /dev/null
+++ b/lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex
@@ -0,0 +1,35 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug do
+ import Pleroma.Web.TranslationHelpers
+ import Plug.Conn
+
+ alias Pleroma.Config
+ alias Pleroma.User
+
+ use Pleroma.Web, :plug
+
+ def init(options) do
+ options
+ end
+
+ @impl true
+ def perform(conn, _) do
+ public? = Config.get!([:instance, :public])
+
+ case {public?, conn} do
+ {true, _} ->
+ conn
+
+ {false, %{assigns: %{user: %User{}}}} ->
+ conn
+
+ {false, _} ->
+ conn
+ |> render_error(:forbidden, "This resource requires authentication.")
+ |> halt
+ end
+ end
+end
diff --git a/lib/pleroma/web/plugs/ensure_user_key_plug.ex b/lib/pleroma/web/plugs/ensure_user_key_plug.ex
new file mode 100644
index 000000000..70d3091f0
--- /dev/null
+++ b/lib/pleroma/web/plugs/ensure_user_key_plug.ex
@@ -0,0 +1,18 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.EnsureUserKeyPlug do
+ import Plug.Conn
+
+ def init(opts) do
+ opts
+ end
+
+ def call(%{assigns: %{user: _}} = conn, _), do: conn
+
+ def call(conn, _) do
+ conn
+ |> assign(:user, nil)
+ end
+end
diff --git a/lib/pleroma/web/plugs/expect_authenticated_check_plug.ex b/lib/pleroma/web/plugs/expect_authenticated_check_plug.ex
new file mode 100644
index 000000000..0925ded4d
--- /dev/null
+++ b/lib/pleroma/web/plugs/expect_authenticated_check_plug.ex
@@ -0,0 +1,20 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug do
+ @moduledoc """
+ Marks `Pleroma.Web.Plugs.EnsureAuthenticatedPlug` as expected to be executed later in plug chain.
+
+ No-op plug which affects `Pleroma.Web` operation (is checked with `PlugHelper.plug_called?/2`).
+ """
+
+ use Pleroma.Web, :plug
+
+ def init(options), do: options
+
+ @impl true
+ def perform(conn, _) do
+ conn
+ end
+end
diff --git a/lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex b/lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex
new file mode 100644
index 000000000..ace512a78
--- /dev/null
+++ b/lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex
@@ -0,0 +1,21 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug do
+ @moduledoc """
+ Marks `Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug` as expected to be executed later in plug
+ chain.
+
+ No-op plug which affects `Pleroma.Web` operation (is checked with `PlugHelper.plug_called?/2`).
+ """
+
+ use Pleroma.Web, :plug
+
+ def init(options), do: options
+
+ @impl true
+ def perform(conn, _) do
+ conn
+ end
+end
diff --git a/lib/pleroma/web/plugs/federating_plug.ex b/lib/pleroma/web/plugs/federating_plug.ex
new file mode 100644
index 000000000..3c90a7644
--- /dev/null
+++ b/lib/pleroma/web/plugs/federating_plug.ex
@@ -0,0 +1,32 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.FederatingPlug do
+ import Plug.Conn
+
+ def init(options) do
+ options
+ end
+
+ def call(conn, _opts) do
+ if federating?() do
+ conn
+ else
+ fail(conn)
+ end
+ end
+
+ def federating?, do: Pleroma.Config.get([:instance, :federating])
+
+ # Definition for the use in :if_func / :unless_func plug options
+ def federating?(_conn), do: federating?()
+
+ defp fail(conn) do
+ conn
+ |> put_status(404)
+ |> Phoenix.Controller.put_view(Pleroma.Web.ErrorView)
+ |> Phoenix.Controller.render("404.json")
+ |> halt()
+ end
+end
diff --git a/lib/pleroma/web/plugs/frontend_static.ex b/lib/pleroma/web/plugs/frontend_static.ex
new file mode 100644
index 000000000..1b0b36813
--- /dev/null
+++ b/lib/pleroma/web/plugs/frontend_static.ex
@@ -0,0 +1,59 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.FrontendStatic do
+ require Pleroma.Constants
+
+ @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")
+
+ Path.join([
+ instance_static_path,
+ "frontends",
+ configuration["name"],
+ configuration["ref"],
+ path
+ ])
+ else
+ nil
+ end
+ end
+
+ def init(opts) do
+ opts
+ |> Keyword.put(:from, "__unconfigured_frontend_static_plug")
+ |> Plug.Static.init()
+ |> Map.put(:frontend_type, opts[:frontend_type])
+ end
+
+ def call(conn, opts) do
+ with false <- invalid_path?(conn.path_info),
+ frontend_type <- Map.get(opts, :frontend_type, :primary),
+ path when not is_nil(path) <- file_path("", frontend_type) do
+ call_static(conn, opts, path)
+ else
+ _ ->
+ conn
+ end
+ end
+
+ defp invalid_path?(list) do
+ invalid_path?(list, :binary.compile_pattern(["/", "\\", ":", "\0"]))
+ end
+
+ defp invalid_path?([h | _], _match) when h in [".", "..", ""], do: true
+ defp invalid_path?([h | t], match), do: String.contains?(h, match) or invalid_path?(t)
+ defp invalid_path?([], _match), do: false
+
+ defp call_static(conn, opts, from) do
+ opts = Map.put(opts, :from, from)
+ Plug.Static.call(conn, opts)
+ end
+end
diff --git a/lib/pleroma/web/plugs/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex
new file mode 100644
index 000000000..45aaf188e
--- /dev/null
+++ b/lib/pleroma/web/plugs/http_security_plug.ex
@@ -0,0 +1,225 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do
+ alias Pleroma.Config
+ import Plug.Conn
+
+ require Logger
+
+ def init(opts), do: opts
+
+ def call(conn, _options) do
+ if Config.get([:http_security, :enabled]) do
+ conn
+ |> merge_resp_headers(headers())
+ |> maybe_send_sts_header(Config.get([:http_security, :sts]))
+ else
+ conn
+ end
+ end
+
+ defp headers do
+ referrer_policy = Config.get([:http_security, :referrer_policy])
+ report_uri = Config.get([:http_security, :report_uri])
+
+ headers = [
+ {"x-xss-protection", "1; mode=block"},
+ {"x-permitted-cross-domain-policies", "none"},
+ {"x-frame-options", "DENY"},
+ {"x-content-type-options", "nosniff"},
+ {"referrer-policy", referrer_policy},
+ {"x-download-options", "noopen"},
+ {"content-security-policy", csp_string()}
+ ]
+
+ if report_uri do
+ report_group = %{
+ "group" => "csp-endpoint",
+ "max-age" => 10_886_400,
+ "endpoints" => [
+ %{"url" => report_uri}
+ ]
+ }
+
+ [{"reply-to", Jason.encode!(report_group)} | headers]
+ else
+ headers
+ end
+ end
+
+ static_csp_rules = [
+ "default-src 'none'",
+ "base-uri 'self'",
+ "frame-ancestors 'none'",
+ "style-src 'self' 'unsafe-inline'",
+ "font-src 'self'",
+ "manifest-src 'self'"
+ ]
+
+ @csp_start [Enum.join(static_csp_rules, ";") <> ";"]
+
+ defp csp_string do
+ scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme]
+ static_url = Pleroma.Web.Endpoint.static_url()
+ websocket_url = Pleroma.Web.Endpoint.websocket_url()
+ report_uri = Config.get([:http_security, :report_uri])
+
+ img_src = "img-src 'self' data: blob:"
+ media_src = "media-src 'self'"
+
+ # Strict multimedia CSP enforcement only when MediaProxy is enabled
+ {img_src, media_src} =
+ if Config.get([:media_proxy, :enabled]) &&
+ !Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do
+ sources = build_csp_multimedia_source_list()
+ {[img_src, sources], [media_src, sources]}
+ else
+ {[img_src, " https:"], [media_src, " https:"]}
+ end
+
+ connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url]
+
+ connect_src =
+ if Config.get(:env) == :dev do
+ [connect_src, " http://localhost:3035/"]
+ else
+ connect_src
+ end
+
+ script_src =
+ if Config.get(:env) == :dev do
+ "script-src 'self' 'unsafe-eval'"
+ else
+ "script-src 'self'"
+ end
+
+ report = if report_uri, do: ["report-uri ", report_uri, ";report-to csp-endpoint"]
+ insecure = if scheme == "https", do: "upgrade-insecure-requests"
+
+ @csp_start
+ |> add_csp_param(img_src)
+ |> add_csp_param(media_src)
+ |> add_csp_param(connect_src)
+ |> add_csp_param(script_src)
+ |> add_csp_param(insecure)
+ |> add_csp_param(report)
+ |> :erlang.iolist_to_binary()
+ end
+
+ defp build_csp_from_whitelist([], acc), do: acc
+
+ defp build_csp_from_whitelist([last], acc) do
+ [build_csp_param_from_whitelist(last) | acc]
+ end
+
+ defp build_csp_from_whitelist([head | tail], acc) do
+ build_csp_from_whitelist(tail, [[?\s, build_csp_param_from_whitelist(head)] | acc])
+ end
+
+ # TODO: use `build_csp_param/1` after removing support bare domains for media proxy whitelist
+ defp build_csp_param_from_whitelist("http" <> _ = url) do
+ build_csp_param(url)
+ end
+
+ defp build_csp_param_from_whitelist(url), do: url
+
+ defp build_csp_multimedia_source_list do
+ media_proxy_whitelist =
+ [:media_proxy, :whitelist]
+ |> Config.get()
+ |> build_csp_from_whitelist([])
+
+ captcha_method = Config.get([Pleroma.Captcha, :method])
+ captcha_endpoint = Config.get([captcha_method, :endpoint])
+
+ base_endpoints =
+ [
+ [:media_proxy, :base_url],
+ [Pleroma.Upload, :base_url],
+ [Pleroma.Uploaders.S3, :public_endpoint]
+ ]
+ |> Enum.map(&Config.get/1)
+
+ [captcha_endpoint | base_endpoints]
+ |> Enum.map(&build_csp_param/1)
+ |> Enum.reduce([], &add_source(&2, &1))
+ |> add_source(media_proxy_whitelist)
+ end
+
+ defp add_source(iodata, nil), do: iodata
+ defp add_source(iodata, []), do: iodata
+ defp add_source(iodata, source), do: [[?\s, source] | iodata]
+
+ defp add_csp_param(csp_iodata, nil), do: csp_iodata
+
+ defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata]
+
+ defp build_csp_param(nil), do: nil
+
+ defp build_csp_param(url) when is_binary(url) do
+ %{host: host, scheme: scheme} = URI.parse(url)
+
+ if scheme do
+ [scheme, "://", host]
+ end
+ end
+
+ def warn_if_disabled do
+ unless Config.get([:http_security, :enabled]) do
+ Logger.warn("
+ .i;;;;i.
+ iYcviii;vXY:
+ .YXi .i1c.
+ .YC. . in7.
+ .vc. ...... ;1c.
+ i7, .. .;1;
+ i7, .. ... .Y1i
+ ,7v .6MMM@; .YX,
+ .7;. ..IMMMMMM1 :t7.
+ .;Y. ;$MMMMMM9. :tc.
+ vY. .. .nMMM@MMU. ;1v.
+ i7i ... .#MM@M@C. .....:71i
+ it: .... $MMM@9;.,i;;;i,;tti
+ :t7. ..... 0MMMWv.,iii:::,,;St.
+ .nC. ..... IMMMQ..,::::::,.,czX.
+ .ct: ....... .ZMMMI..,:::::::,,:76Y.
+ c2: ......,i..Y$M@t..:::::::,,..inZY
+ vov ......:ii..c$MBc..,,,,,,,,,,..iI9i
+ i9Y ......iii:..7@MA,..,,,,,,,,,....;AA:
+ iIS. ......:ii::..;@MI....,............;Ez.
+ .I9. ......:i::::...8M1..................C0z.
+ .z9; ......:i::::,.. .i:...................zWX.
+ vbv ......,i::::,,. ................. :AQY
+ c6Y. .,...,::::,,..:t0@@QY. ................ :8bi
+ :6S. ..,,...,:::,,,..EMMMMMMI. ............... .;bZ,
+ :6o, .,,,,..:::,,,..i#MMMMMM#v................. YW2.
+ .n8i ..,,,,,,,::,,,,.. tMMMMM@C:.................. .1Wn
+ 7Uc. .:::,,,,,::,,,,.. i1t;,..................... .UEi
+ 7C...::::::::::::,,,,.. .................... vSi.
+ ;1;...,,::::::,......... .................. Yz:
+ v97,......... .voC.
+ izAotX7777777777777777777777777777777777777777Y7n92:
+ .;CoIIIIIUAA666666699999ZZZZZZZZZZZZZZZZZZZZ6ov.
+
+HTTP Security is disabled. Please re-enable it to prevent users from attacking
+your instance and your users via malicious posts:
+
+ config :pleroma, :http_security, enabled: true
+ ")
+ end
+ end
+
+ defp maybe_send_sts_header(conn, true) do
+ max_age_sts = Config.get([:http_security, :sts_max_age])
+ max_age_ct = Config.get([:http_security, :ct_max_age])
+
+ merge_resp_headers(conn, [
+ {"strict-transport-security", "max-age=#{max_age_sts}; includeSubDomains"},
+ {"expect-ct", "enforce, max-age=#{max_age_ct}"}
+ ])
+ end
+
+ defp maybe_send_sts_header(conn, _), do: conn
+end
diff --git a/lib/pleroma/web/plugs/http_signature_plug.ex b/lib/pleroma/web/plugs/http_signature_plug.ex
new file mode 100644
index 000000000..036e2a773
--- /dev/null
+++ b/lib/pleroma/web/plugs/http_signature_plug.ex
@@ -0,0 +1,65 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
+ import Plug.Conn
+ import Phoenix.Controller, only: [get_format: 1, text: 2]
+ require Logger
+
+ def init(options) do
+ options
+ end
+
+ def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
+ conn
+ end
+
+ def call(conn, _opts) do
+ if get_format(conn) == "activity+json" do
+ conn
+ |> maybe_assign_valid_signature()
+ |> maybe_require_signature()
+ else
+ conn
+ end
+ end
+
+ defp maybe_assign_valid_signature(conn) do
+ if has_signature_header?(conn) do
+ # set (request-target) header to the appropriate value
+ # we also replace the digest header with the one we computed
+ request_target = String.downcase("#{conn.method}") <> " #{conn.request_path}"
+
+ conn =
+ conn
+ |> put_req_header("(request-target)", request_target)
+ |> case do
+ %{assigns: %{digest: digest}} = conn -> put_req_header(conn, "digest", digest)
+ conn -> conn
+ end
+
+ assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn))
+ else
+ Logger.debug("No signature header!")
+ conn
+ end
+ end
+
+ defp has_signature_header?(conn) do
+ conn |> get_req_header("signature") |> Enum.at(0, false)
+ end
+
+ defp maybe_require_signature(%{assigns: %{valid_signature: true}} = conn), do: conn
+
+ defp maybe_require_signature(conn) do
+ if Pleroma.Config.get([:activitypub, :authorized_fetch_mode], false) do
+ conn
+ |> put_status(:unauthorized)
+ |> text("Request not signed")
+ |> halt()
+ else
+ conn
+ end
+ end
+end
diff --git a/lib/pleroma/web/plugs/idempotency_plug.ex b/lib/pleroma/web/plugs/idempotency_plug.ex
new file mode 100644
index 000000000..254a790b0
--- /dev/null
+++ b/lib/pleroma/web/plugs/idempotency_plug.ex
@@ -0,0 +1,84 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.IdempotencyPlug do
+ import Phoenix.Controller, only: [json: 2]
+ import Plug.Conn
+
+ @behaviour Plug
+
+ @impl true
+ def init(opts), do: opts
+
+ # Sending idempotency keys in `GET` and `DELETE` requests has no effect
+ # and should be avoided, as these requests are idempotent by definition.
+
+ @impl true
+ def call(%{method: method} = conn, _) when method in ["POST", "PUT", "PATCH"] do
+ case get_req_header(conn, "idempotency-key") do
+ [key] -> process_request(conn, key)
+ _ -> conn
+ end
+ end
+
+ def call(conn, _), do: conn
+
+ def process_request(conn, key) do
+ case Cachex.get(:idempotency_cache, key) do
+ {:ok, nil} ->
+ cache_resposnse(conn, key)
+
+ {:ok, record} ->
+ send_cached(conn, key, record)
+
+ {atom, message} when atom in [:ignore, :error] ->
+ render_error(conn, message)
+ end
+ end
+
+ defp cache_resposnse(conn, key) do
+ register_before_send(conn, fn conn ->
+ [request_id] = get_resp_header(conn, "x-request-id")
+ content_type = get_content_type(conn)
+
+ record = {request_id, content_type, conn.status, conn.resp_body}
+ {:ok, _} = Cachex.put(:idempotency_cache, key, record)
+
+ conn
+ |> put_resp_header("idempotency-key", key)
+ |> put_resp_header("x-original-request-id", request_id)
+ end)
+ end
+
+ defp send_cached(conn, key, record) do
+ {request_id, content_type, status, body} = record
+
+ conn
+ |> put_resp_header("idempotency-key", key)
+ |> put_resp_header("idempotent-replayed", "true")
+ |> put_resp_header("x-original-request-id", request_id)
+ |> put_resp_content_type(content_type)
+ |> send_resp(status, body)
+ |> halt()
+ end
+
+ defp render_error(conn, message) do
+ conn
+ |> put_status(:unprocessable_entity)
+ |> json(%{error: message})
+ |> halt()
+ end
+
+ defp get_content_type(conn) do
+ [content_type] = get_resp_header(conn, "content-type")
+
+ if String.contains?(content_type, ";") do
+ content_type
+ |> String.split(";")
+ |> hd()
+ else
+ content_type
+ end
+ end
+end
diff --git a/lib/pleroma/web/plugs/instance_static.ex b/lib/pleroma/web/plugs/instance_static.ex
new file mode 100644
index 000000000..54b9175df
--- /dev/null
+++ b/lib/pleroma/web/plugs/instance_static.ex
@@ -0,0 +1,53 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.InstanceStatic do
+ require Pleroma.Constants
+
+ @moduledoc """
+ This is a shim to call `Plug.Static` but with runtime `from` configuration.
+
+ Mountpoints are defined directly in the module to avoid calling the configuration for every request including non-static ones.
+ """
+ @behaviour Plug
+
+ def file_path(path) do
+ instance_path =
+ Path.join(Pleroma.Config.get([:instance, :static_dir], "instance/static/"), path)
+
+ frontend_path = Pleroma.Web.Plugs.FrontendStatic.file_path(path, :primary)
+
+ (File.exists?(instance_path) && instance_path) ||
+ (frontend_path && File.exists?(frontend_path) && frontend_path) ||
+ Path.join(Application.app_dir(:pleroma, "priv/static/"), path)
+ end
+
+ def init(opts) do
+ opts
+ |> Keyword.put(:from, "__unconfigured_instance_static_plug")
+ |> Plug.Static.init()
+ end
+
+ for only <- Pleroma.Constants.static_only_files() do
+ def call(%{request_path: "/" <> unquote(only) <> _} = conn, opts) do
+ call_static(
+ conn,
+ opts,
+ Pleroma.Config.get([:instance, :static_dir], "instance/static")
+ )
+ end
+ end
+
+ def call(conn, _) do
+ conn
+ end
+
+ defp call_static(conn, opts, from) do
+ opts =
+ opts
+ |> Map.put(:from, from)
+
+ Plug.Static.call(conn, opts)
+ end
+end
diff --git a/lib/pleroma/web/plugs/legacy_authentication_plug.ex b/lib/pleroma/web/plugs/legacy_authentication_plug.ex
new file mode 100644
index 000000000..2a54d0b59
--- /dev/null
+++ b/lib/pleroma/web/plugs/legacy_authentication_plug.ex
@@ -0,0 +1,41 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.LegacyAuthenticationPlug do
+ import Plug.Conn
+
+ alias Pleroma.User
+
+ def init(options) do
+ options
+ end
+
+ def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
+
+ def call(
+ %{
+ assigns: %{
+ auth_user: %{password_hash: "$6$" <> _ = password_hash} = auth_user,
+ auth_credentials: %{password: password}
+ }
+ } = conn,
+ _
+ ) do
+ with ^password_hash <- :crypt.crypt(password, password_hash),
+ {:ok, user} <-
+ User.reset_password(auth_user, %{password: password, password_confirmation: password}) do
+ conn
+ |> assign(:auth_user, user)
+ |> assign(:user, user)
+ |> Pleroma.Web.Plugs.OAuthScopesPlug.skip_plug()
+ else
+ _ ->
+ conn
+ end
+ end
+
+ def call(conn, _) do
+ conn
+ end
+end
diff --git a/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex
new file mode 100644
index 000000000..f44d4dee5
--- /dev/null
+++ b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex
@@ -0,0 +1,71 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
+ alias Pleroma.Signature
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.Utils
+
+ import Plug.Conn
+ require Logger
+
+ def init(options), do: options
+
+ defp key_id_from_conn(conn) do
+ with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn),
+ {:ok, ap_id} <- Signature.key_id_to_actor_id(key_id) do
+ ap_id
+ else
+ _ ->
+ nil
+ end
+ end
+
+ defp user_from_key_id(conn) do
+ with key_actor_id when is_binary(key_actor_id) <- key_id_from_conn(conn),
+ {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(key_actor_id) do
+ user
+ else
+ _ ->
+ nil
+ end
+ end
+
+ def call(%{assigns: %{user: _}} = conn, _opts), do: conn
+
+ # if this has payload make sure it is signed by the same actor that made it
+ def call(%{assigns: %{valid_signature: true}, params: %{"actor" => actor}} = conn, _opts) do
+ with actor_id <- Utils.get_ap_id(actor),
+ {:user, %User{} = user} <- {:user, user_from_key_id(conn)},
+ {:user_match, true} <- {:user_match, user.ap_id == actor_id} do
+ assign(conn, :user, user)
+ else
+ {:user_match, false} ->
+ Logger.debug("Failed to map identity from signature (payload actor mismatch)")
+ Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{inspect(actor)}")
+ assign(conn, :valid_signature, false)
+
+ # remove me once testsuite uses mapped capabilities instead of what we do now
+ {:user, nil} ->
+ Logger.debug("Failed to map identity from signature (lookup failure)")
+ Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}")
+ conn
+ end
+ end
+
+ # no payload, probably a signed fetch
+ def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
+ with %User{} = user <- user_from_key_id(conn) do
+ assign(conn, :user, user)
+ else
+ _ ->
+ Logger.debug("Failed to map identity from signature (no payload actor mismatch)")
+ Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}")
+ assign(conn, :valid_signature, false)
+ end
+ end
+
+ # no signature at all
+ def call(conn, _opts), do: conn
+end
diff --git a/lib/pleroma/web/plugs/o_auth_plug.ex b/lib/pleroma/web/plugs/o_auth_plug.ex
new file mode 100644
index 000000000..c7b58d90f
--- /dev/null
+++ b/lib/pleroma/web/plugs/o_auth_plug.ex
@@ -0,0 +1,120 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.OAuthPlug do
+ import Plug.Conn
+ import Ecto.Query
+
+ alias Pleroma.Repo
+ alias Pleroma.User
+ alias Pleroma.Web.OAuth.App
+ alias Pleroma.Web.OAuth.Token
+
+ @realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i")
+
+ def init(options), do: options
+
+ def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
+
+ def call(%{params: %{"access_token" => access_token}} = conn, _) do
+ with {:ok, user, token_record} <- fetch_user_and_token(access_token) do
+ conn
+ |> assign(:token, token_record)
+ |> assign(:user, user)
+ else
+ _ ->
+ # token found, but maybe only with app
+ with {:ok, app, token_record} <- fetch_app_and_token(access_token) do
+ conn
+ |> assign(:token, token_record)
+ |> assign(:app, app)
+ else
+ _ -> conn
+ end
+ end
+ end
+
+ def call(conn, _) do
+ case fetch_token_str(conn) do
+ {:ok, token} ->
+ with {:ok, user, token_record} <- fetch_user_and_token(token) do
+ conn
+ |> assign(:token, token_record)
+ |> assign(:user, user)
+ else
+ _ ->
+ # token found, but maybe only with app
+ with {:ok, app, token_record} <- fetch_app_and_token(token) do
+ conn
+ |> assign(:token, token_record)
+ |> assign(:app, app)
+ else
+ _ -> conn
+ end
+ end
+
+ _ ->
+ conn
+ end
+ end
+
+ # Gets user by token
+ #
+ @spec fetch_user_and_token(String.t()) :: {:ok, User.t(), Token.t()} | nil
+ defp fetch_user_and_token(token) do
+ query =
+ from(t in Token,
+ where: t.token == ^token,
+ join: user in assoc(t, :user),
+ preload: [user: user]
+ )
+
+ # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
+ with %Token{user: user} = token_record <- Repo.one(query) do
+ {:ok, user, token_record}
+ end
+ end
+
+ @spec fetch_app_and_token(String.t()) :: {:ok, App.t(), Token.t()} | nil
+ defp fetch_app_and_token(token) do
+ query =
+ from(t in Token, where: t.token == ^token, join: app in assoc(t, :app), preload: [app: app])
+
+ with %Token{app: app} = token_record <- Repo.one(query) do
+ {:ok, app, token_record}
+ end
+ end
+
+ # Gets token from session by :oauth_token key
+ #
+ @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
+ defp fetch_token_from_session(conn) do
+ case get_session(conn, :oauth_token) do
+ nil -> :no_token_found
+ token -> {:ok, token}
+ end
+ end
+
+ # Gets token from headers
+ #
+ @spec fetch_token_str(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
+ defp fetch_token_str(%Plug.Conn{} = conn) do
+ headers = get_req_header(conn, "authorization")
+
+ with :no_token_found <- fetch_token_str(headers),
+ do: fetch_token_from_session(conn)
+ end
+
+ @spec fetch_token_str(Keyword.t()) :: :no_token_found | {:ok, String.t()}
+ defp fetch_token_str([]), do: :no_token_found
+
+ defp fetch_token_str([token | tail]) do
+ trimmed_token = String.trim(token)
+
+ case Regex.run(@realm_reg, trimmed_token) do
+ [_, match] -> {:ok, String.trim(match)}
+ _ -> fetch_token_str(tail)
+ end
+ end
+end
diff --git a/lib/pleroma/web/plugs/o_auth_scopes_plug.ex b/lib/pleroma/web/plugs/o_auth_scopes_plug.ex
new file mode 100644
index 000000000..cfc30837c
--- /dev/null
+++ b/lib/pleroma/web/plugs/o_auth_scopes_plug.ex
@@ -0,0 +1,77 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.OAuthScopesPlug do
+ import Plug.Conn
+ import Pleroma.Web.Gettext
+
+ alias Pleroma.Config
+
+ use Pleroma.Web, :plug
+
+ def init(%{scopes: _} = options), do: options
+
+ @impl true
+ def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
+ op = options[:op] || :|
+ token = assigns[:token]
+
+ scopes = transform_scopes(scopes, options)
+ matched_scopes = (token && filter_descendants(scopes, token.scopes)) || []
+
+ cond do
+ token && op == :| && Enum.any?(matched_scopes) ->
+ conn
+
+ token && op == :& && matched_scopes == scopes ->
+ conn
+
+ options[:fallback] == :proceed_unauthenticated ->
+ drop_auth_info(conn)
+
+ true ->
+ missing_scopes = scopes -- matched_scopes
+ permissions = Enum.join(missing_scopes, " #{op} ")
+
+ error_message =
+ dgettext("errors", "Insufficient permissions: %{permissions}.", permissions: permissions)
+
+ conn
+ |> put_resp_content_type("application/json")
+ |> send_resp(:forbidden, Jason.encode!(%{error: error_message}))
+ |> halt()
+ end
+ end
+
+ @doc "Drops authentication info from connection"
+ def drop_auth_info(conn) do
+ # To simplify debugging, setting a private variable on `conn` if auth info is dropped
+ conn
+ |> put_private(:authentication_ignored, true)
+ |> assign(:user, nil)
+ |> assign(:token, nil)
+ end
+
+ @doc "Keeps those of `scopes` which are descendants of `supported_scopes`"
+ def filter_descendants(scopes, supported_scopes) do
+ Enum.filter(
+ scopes,
+ fn scope ->
+ Enum.find(
+ supported_scopes,
+ &(scope == &1 || String.starts_with?(scope, &1 <> ":"))
+ )
+ end
+ )
+ end
+
+ @doc "Transforms scopes by applying supported options (e.g. :admin)"
+ def transform_scopes(scopes, options) do
+ if options[:admin] do
+ Config.oauth_admin_scopes(scopes)
+ else
+ scopes
+ end
+ end
+end
diff --git a/lib/pleroma/web/plugs/plug_helper.ex b/lib/pleroma/web/plugs/plug_helper.ex
new file mode 100644
index 000000000..b314e7596
--- /dev/null
+++ b/lib/pleroma/web/plugs/plug_helper.ex
@@ -0,0 +1,40 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.PlugHelper do
+ @moduledoc "Pleroma Plug helper"
+
+ @called_plugs_list_id :called_plugs
+ def called_plugs_list_id, do: @called_plugs_list_id
+
+ @skipped_plugs_list_id :skipped_plugs
+ def skipped_plugs_list_id, do: @skipped_plugs_list_id
+
+ @doc "Returns `true` if specified plug was called."
+ def plug_called?(conn, plug_module) do
+ contained_in_private_list?(conn, @called_plugs_list_id, plug_module)
+ end
+
+ @doc "Returns `true` if specified plug was explicitly marked as skipped."
+ def plug_skipped?(conn, plug_module) do
+ contained_in_private_list?(conn, @skipped_plugs_list_id, plug_module)
+ end
+
+ @doc "Returns `true` if specified plug was either called or explicitly marked as skipped."
+ def plug_called_or_skipped?(conn, plug_module) do
+ plug_called?(conn, plug_module) || plug_skipped?(conn, plug_module)
+ end
+
+ # Appends plug to known list (skipped, called). Intended to be used from within plug code only.
+ def append_to_private_list(conn, list_id, value) do
+ list = conn.private[list_id] || []
+ modified_list = Enum.uniq(list ++ [value])
+ Plug.Conn.put_private(conn, list_id, modified_list)
+ end
+
+ defp contained_in_private_list?(conn, private_variable, value) do
+ list = conn.private[private_variable] || []
+ value in list
+ end
+end
diff --git a/lib/pleroma/web/plugs/rate_limiter.ex b/lib/pleroma/web/plugs/rate_limiter.ex
new file mode 100644
index 000000000..a589610d1
--- /dev/null
+++ b/lib/pleroma/web/plugs/rate_limiter.ex
@@ -0,0 +1,267 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.RateLimiter do
+ @moduledoc """
+
+ ## Configuration
+
+ A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration.
+ The basic configuration is a tuple where:
+
+ * The first element: `scale` (Integer). The time scale in milliseconds.
+ * The second element: `limit` (Integer). How many requests to limit in the time scale provided.
+
+ It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a
+ list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated.
+
+ To disable a limiter set its value to `nil`.
+
+ ### Example
+
+ config :pleroma, :rate_limit,
+ one: {1000, 10},
+ two: [{10_000, 10}, {10_000, 50}],
+ foobar: nil
+
+ Here we have three limiters:
+
+ * `one` which is not over 10req/1s
+ * `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users
+ * `foobar` which is disabled
+
+ ## Usage
+
+ AllowedSyntax:
+
+ plug(Pleroma.Web.Plugs.RateLimiter, name: :limiter_name)
+ plug(Pleroma.Web.Plugs.RateLimiter, options) # :name is a required option
+
+ Allowed options:
+
+ * `name` required, always used to fetch the limit values from the config
+ * `bucket_name` overrides name for counting purposes (e.g. to have a separate limit for a set of actions)
+ * `params` appends values of specified request params (e.g. ["id"]) to bucket name
+
+ Inside a controller:
+
+ plug(Pleroma.Web.Plugs.RateLimiter, [name: :one] when action == :one)
+ plug(Pleroma.Web.Plugs.RateLimiter, [name: :two] when action in [:two, :three])
+
+ plug(
+ Pleroma.Web.Plugs.RateLimiter,
+ [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]]
+ when action in ~w(fav_status unfav_status)a
+ )
+
+ or inside a router pipeline:
+
+ pipeline :api do
+ ...
+ plug(Pleroma.Web.Plugs.RateLimiter, name: :one)
+ ...
+ end
+ """
+ import Pleroma.Web.TranslationHelpers
+ import Plug.Conn
+
+ alias Pleroma.Config
+ alias Pleroma.User
+ alias Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor
+
+ require Logger
+
+ @doc false
+ def init(plug_opts) do
+ plug_opts
+ end
+
+ def call(conn, plug_opts) do
+ if disabled?(conn) do
+ handle_disabled(conn)
+ else
+ action_settings = action_settings(plug_opts)
+ handle(conn, action_settings)
+ end
+ end
+
+ defp handle_disabled(conn) do
+ Logger.warn(
+ "Rate limiter disabled due to forwarded IP not being found. Please ensure your reverse proxy is providing the X-Forwarded-For header or disable the RemoteIP plug/rate limiter."
+ )
+
+ conn
+ end
+
+ defp handle(conn, nil), do: conn
+
+ defp handle(conn, action_settings) do
+ action_settings
+ |> incorporate_conn_info(conn)
+ |> check_rate()
+ |> case do
+ {:ok, _count} ->
+ conn
+
+ {:error, _count} ->
+ render_throttled_error(conn)
+ end
+ end
+
+ def disabled?(conn) do
+ if Map.has_key?(conn.assigns, :remote_ip_found),
+ do: !conn.assigns.remote_ip_found,
+ else: false
+ end
+
+ @inspect_bucket_not_found {:error, :not_found}
+
+ def inspect_bucket(conn, bucket_name_root, plug_opts) do
+ with %{name: _} = action_settings <- action_settings(plug_opts) do
+ action_settings = incorporate_conn_info(action_settings, conn)
+ bucket_name = make_bucket_name(%{action_settings | name: bucket_name_root})
+ key_name = make_key_name(action_settings)
+ limit = get_limits(action_settings)
+
+ case Cachex.get(bucket_name, key_name) do
+ {:error, :no_cache} ->
+ @inspect_bucket_not_found
+
+ {:ok, nil} ->
+ {0, limit}
+
+ {:ok, value} ->
+ {value, limit - value}
+ end
+ else
+ _ -> @inspect_bucket_not_found
+ end
+ end
+
+ def action_settings(plug_opts) do
+ with limiter_name when is_atom(limiter_name) <- plug_opts[:name],
+ limits when not is_nil(limits) <- Config.get([:rate_limit, limiter_name]) do
+ bucket_name_root = Keyword.get(plug_opts, :bucket_name, limiter_name)
+
+ %{
+ name: bucket_name_root,
+ limits: limits,
+ opts: plug_opts
+ }
+ end
+ end
+
+ defp check_rate(action_settings) do
+ bucket_name = make_bucket_name(action_settings)
+ key_name = make_key_name(action_settings)
+ limit = get_limits(action_settings)
+
+ case Cachex.get_and_update(bucket_name, key_name, &increment_value(&1, limit)) do
+ {:commit, value} ->
+ {:ok, value}
+
+ {:ignore, value} ->
+ {:error, value}
+
+ {:error, :no_cache} ->
+ initialize_buckets!(action_settings)
+ check_rate(action_settings)
+ end
+ end
+
+ defp increment_value(nil, _limit), do: {:commit, 1}
+
+ defp increment_value(val, limit) when val >= limit, do: {:ignore, val}
+
+ defp increment_value(val, _limit), do: {:commit, val + 1}
+
+ defp incorporate_conn_info(action_settings, %{
+ assigns: %{user: %User{id: user_id}},
+ params: params
+ }) do
+ Map.merge(action_settings, %{
+ mode: :user,
+ conn_params: params,
+ conn_info: "#{user_id}"
+ })
+ end
+
+ defp incorporate_conn_info(action_settings, %{params: params} = conn) do
+ Map.merge(action_settings, %{
+ mode: :anon,
+ conn_params: params,
+ conn_info: "#{ip(conn)}"
+ })
+ end
+
+ defp ip(%{remote_ip: remote_ip}) do
+ remote_ip
+ |> Tuple.to_list()
+ |> Enum.join(".")
+ end
+
+ defp render_throttled_error(conn) do
+ conn
+ |> render_error(:too_many_requests, "Throttled")
+ |> halt()
+ end
+
+ defp make_key_name(action_settings) do
+ ""
+ |> attach_selected_params(action_settings)
+ |> attach_identity(action_settings)
+ end
+
+ defp get_scale(_, {scale, _}), do: scale
+
+ defp get_scale(:anon, [{scale, _}, {_, _}]), do: scale
+
+ defp get_scale(:user, [{_, _}, {scale, _}]), do: scale
+
+ defp get_limits(%{limits: {_scale, limit}}), do: limit
+
+ defp get_limits(%{mode: :user, limits: [_, {_, limit}]}), do: limit
+
+ defp get_limits(%{limits: [{_, limit}, _]}), do: limit
+
+ defp make_bucket_name(%{mode: :user, name: bucket_name_root}),
+ do: user_bucket_name(bucket_name_root)
+
+ defp make_bucket_name(%{mode: :anon, name: bucket_name_root}),
+ do: anon_bucket_name(bucket_name_root)
+
+ defp attach_selected_params(input, %{conn_params: conn_params, opts: plug_opts}) do
+ params_string =
+ plug_opts
+ |> Keyword.get(:params, [])
+ |> Enum.sort()
+ |> Enum.map(&Map.get(conn_params, &1, ""))
+ |> Enum.join(":")
+
+ [input, params_string]
+ |> Enum.join(":")
+ |> String.replace_leading(":", "")
+ end
+
+ defp initialize_buckets!(%{name: _name, limits: nil}), do: :ok
+
+ defp initialize_buckets!(%{name: name, limits: limits}) do
+ {:ok, _pid} =
+ LimiterSupervisor.add_or_return_limiter(anon_bucket_name(name), get_scale(:anon, limits))
+
+ {:ok, _pid} =
+ LimiterSupervisor.add_or_return_limiter(user_bucket_name(name), get_scale(:user, limits))
+
+ :ok
+ end
+
+ defp attach_identity(base, %{mode: :user, conn_info: conn_info}),
+ do: "user:#{base}:#{conn_info}"
+
+ defp attach_identity(base, %{mode: :anon, conn_info: conn_info}),
+ do: "ip:#{base}:#{conn_info}"
+
+ defp user_bucket_name(bucket_name_root), do: "user:#{bucket_name_root}" |> String.to_atom()
+ defp anon_bucket_name(bucket_name_root), do: "anon:#{bucket_name_root}" |> String.to_atom()
+end
diff --git a/lib/pleroma/web/plugs/rate_limiter/limiter_supervisor.ex b/lib/pleroma/web/plugs/rate_limiter/limiter_supervisor.ex
new file mode 100644
index 000000000..5642bb205
--- /dev/null
+++ b/lib/pleroma/web/plugs/rate_limiter/limiter_supervisor.ex
@@ -0,0 +1,54 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor do
+ use DynamicSupervisor
+
+ import Cachex.Spec
+
+ def start_link(init_arg) do
+ DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
+ end
+
+ def add_or_return_limiter(limiter_name, expiration) do
+ result =
+ DynamicSupervisor.start_child(
+ __MODULE__,
+ %{
+ id: String.to_atom("rl_#{limiter_name}"),
+ start:
+ {Cachex, :start_link,
+ [
+ limiter_name,
+ [
+ expiration:
+ expiration(
+ default: expiration,
+ interval: check_interval(expiration),
+ lazy: true
+ )
+ ]
+ ]}
+ }
+ )
+
+ case result do
+ {:ok, _pid} = result -> result
+ {:error, {:already_started, pid}} -> {:ok, pid}
+ _ -> result
+ end
+ end
+
+ @impl true
+ def init(_init_arg) do
+ DynamicSupervisor.init(strategy: :one_for_one)
+ end
+
+ defp check_interval(exp) do
+ (exp / 2)
+ |> Kernel.trunc()
+ |> Kernel.min(5000)
+ |> Kernel.max(1)
+ end
+end
diff --git a/lib/pleroma/web/plugs/rate_limiter/supervisor.ex b/lib/pleroma/web/plugs/rate_limiter/supervisor.ex
new file mode 100644
index 000000000..a1c84063d
--- /dev/null
+++ b/lib/pleroma/web/plugs/rate_limiter/supervisor.ex
@@ -0,0 +1,20 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.RateLimiter.Supervisor do
+ use Supervisor
+
+ def start_link(opts) do
+ Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
+ end
+
+ def init(_args) do
+ children = [
+ Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor
+ ]
+
+ opts = [strategy: :one_for_one, name: Pleroma.Web.Streamer.Supervisor]
+ Supervisor.init(children, opts)
+ end
+end
diff --git a/lib/pleroma/web/plugs/remote_ip.ex b/lib/pleroma/web/plugs/remote_ip.ex
new file mode 100644
index 000000000..401e2cbfa
--- /dev/null
+++ b/lib/pleroma/web/plugs/remote_ip.ex
@@ -0,0 +1,48 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+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.
+ """
+
+ alias Pleroma.Config
+ import Plug.Conn
+
+ @behaviour Plug
+
+ def init(_), do: nil
+
+ 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())
+ assign(conn, :remote_ip_found, original_remote_ip != new_remote_ip)
+ else
+ conn
+ end
+ end
+
+ defp remote_ip_opts do
+ headers = Config.get([__MODULE__, :headers], []) |> MapSet.new()
+ reserved = Config.get([__MODULE__, :reserved], [])
+
+ proxies =
+ Config.get([__MODULE__, :proxies], [])
+ |> Enum.concat(reserved)
+ |> Enum.map(&maybe_add_cidr/1)
+
+ {headers, proxies}
+ end
+
+ defp maybe_add_cidr(proxy) when is_binary(proxy) do
+ proxy =
+ cond do
+ "/" in String.codepoints(proxy) -> proxy
+ InetCidr.v4?(InetCidr.parse_address!(proxy)) -> proxy <> "/32"
+ InetCidr.v6?(InetCidr.parse_address!(proxy)) -> proxy <> "/128"
+ end
+
+ InetCidr.parse(proxy, true)
+ end
+end
diff --git a/lib/pleroma/web/plugs/session_authentication_plug.ex b/lib/pleroma/web/plugs/session_authentication_plug.ex
new file mode 100644
index 000000000..6e176d553
--- /dev/null
+++ b/lib/pleroma/web/plugs/session_authentication_plug.ex
@@ -0,0 +1,21 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.SessionAuthenticationPlug do
+ import Plug.Conn
+
+ def init(options) do
+ options
+ end
+
+ def call(conn, _) do
+ with saved_user_id <- get_session(conn, :user_id),
+ %{auth_user: %{id: ^saved_user_id}} <- conn.assigns do
+ conn
+ |> assign(:user, conn.assigns.auth_user)
+ else
+ _ -> conn
+ end
+ end
+end
diff --git a/lib/pleroma/web/plugs/set_format_plug.ex b/lib/pleroma/web/plugs/set_format_plug.ex
new file mode 100644
index 000000000..c16d2f81d
--- /dev/null
+++ b/lib/pleroma/web/plugs/set_format_plug.ex
@@ -0,0 +1,24 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.SetFormatPlug do
+ import Plug.Conn, only: [assign: 3, fetch_query_params: 1]
+
+ def init(_), do: nil
+
+ def call(conn, _) do
+ case get_format(conn) do
+ nil -> conn
+ format -> assign(conn, :format, format)
+ end
+ end
+
+ defp get_format(conn) do
+ conn.private[:phoenix_format] ||
+ case fetch_query_params(conn) do
+ %{query_params: %{"_format" => format}} -> format
+ _ -> nil
+ end
+ end
+end
diff --git a/lib/pleroma/web/plugs/set_locale_plug.ex b/lib/pleroma/web/plugs/set_locale_plug.ex
new file mode 100644
index 000000000..d9d24b93f
--- /dev/null
+++ b/lib/pleroma/web/plugs/set_locale_plug.ex
@@ -0,0 +1,63 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+# NOTE: this module is based on https://github.com/smeevil/set_locale
+defmodule Pleroma.Web.Plugs.SetLocalePlug do
+ import Plug.Conn, only: [get_req_header: 2, assign: 3]
+
+ def init(_), do: nil
+
+ def call(conn, _) do
+ locale = get_locale_from_header(conn) || Gettext.get_locale()
+ Gettext.put_locale(locale)
+ assign(conn, :locale, locale)
+ end
+
+ defp get_locale_from_header(conn) do
+ conn
+ |> extract_accept_language()
+ |> Enum.find(&supported_locale?/1)
+ end
+
+ defp extract_accept_language(conn) do
+ case get_req_header(conn, "accept-language") do
+ [value | _] ->
+ value
+ |> String.split(",")
+ |> Enum.map(&parse_language_option/1)
+ |> Enum.sort(&(&1.quality > &2.quality))
+ |> Enum.map(& &1.tag)
+ |> Enum.reject(&is_nil/1)
+ |> ensure_language_fallbacks()
+
+ _ ->
+ []
+ end
+ end
+
+ defp supported_locale?(locale) do
+ Pleroma.Web.Gettext
+ |> Gettext.known_locales()
+ |> Enum.member?(locale)
+ end
+
+ defp parse_language_option(string) do
+ captures = Regex.named_captures(~r/^\s?(?<tag>[\w\-]+)(?:;q=(?<quality>[\d\.]+))?$/i, string)
+
+ quality =
+ case Float.parse(captures["quality"] || "1.0") do
+ {val, _} -> val
+ :error -> 1.0
+ end
+
+ %{tag: captures["tag"], quality: quality}
+ end
+
+ defp ensure_language_fallbacks(tags) do
+ Enum.flat_map(tags, fn tag ->
+ [language | _] = String.split(tag, "-")
+ if Enum.member?(tags, language), do: [tag], else: [tag, language]
+ end)
+ end
+end
diff --git a/lib/pleroma/web/plugs/set_user_session_id_plug.ex b/lib/pleroma/web/plugs/set_user_session_id_plug.ex
new file mode 100644
index 000000000..e520159e4
--- /dev/null
+++ b/lib/pleroma/web/plugs/set_user_session_id_plug.ex
@@ -0,0 +1,19 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.SetUserSessionIdPlug do
+ import Plug.Conn
+ alias Pleroma.User
+
+ def init(opts) do
+ opts
+ end
+
+ def call(%{assigns: %{user: %User{id: id}}} = conn, _) do
+ conn
+ |> put_session(:user_id, id)
+ end
+
+ def call(conn, _), do: conn
+end
diff --git a/lib/pleroma/web/plugs/static_fe_plug.ex b/lib/pleroma/web/plugs/static_fe_plug.ex
new file mode 100644
index 000000000..658a1052e
--- /dev/null
+++ b/lib/pleroma/web/plugs/static_fe_plug.ex
@@ -0,0 +1,26 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.StaticFEPlug do
+ import Plug.Conn
+ alias Pleroma.Web.StaticFE.StaticFEController
+
+ def init(options), do: options
+
+ def call(conn, _) do
+ if enabled?() and requires_html?(conn) do
+ conn
+ |> StaticFEController.call(:show)
+ |> halt()
+ else
+ conn
+ end
+ end
+
+ defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false)
+
+ defp requires_html?(conn) do
+ Phoenix.Controller.get_format(conn) == "html"
+ end
+end
diff --git a/lib/pleroma/web/plugs/trailing_format_plug.ex b/lib/pleroma/web/plugs/trailing_format_plug.ex
new file mode 100644
index 000000000..e3f57c14a
--- /dev/null
+++ b/lib/pleroma/web/plugs/trailing_format_plug.ex
@@ -0,0 +1,42 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 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."
+
+ @behaviour Plug
+ @paths [
+ "/api/statusnet",
+ "/api/statuses",
+ "/api/qvitter",
+ "/api/search",
+ "/api/account",
+ "/api/friends",
+ "/api/mutes",
+ "/api/media",
+ "/api/favorites",
+ "/api/blocks",
+ "/api/friendships",
+ "/api/users",
+ "/users",
+ "/nodeinfo",
+ "/api/help",
+ "/api/externalprofile",
+ "/notice",
+ "/api/pleroma/emoji",
+ "/api/oauth_tokens"
+ ]
+
+ def init(opts) do
+ TrailingFormatPlug.init(opts)
+ end
+
+ for path <- @paths do
+ def call(%{request_path: unquote(path) <> _} = conn, opts) do
+ TrailingFormatPlug.call(conn, opts)
+ end
+ end
+
+ def call(conn, _opts), do: conn
+end
diff --git a/lib/pleroma/web/plugs/uploaded_media.ex b/lib/pleroma/web/plugs/uploaded_media.ex
new file mode 100644
index 000000000..402a8bb34
--- /dev/null
+++ b/lib/pleroma/web/plugs/uploaded_media.ex
@@ -0,0 +1,107 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.UploadedMedia do
+ @moduledoc """
+ """
+
+ import Plug.Conn
+ import Pleroma.Web.Gettext
+ require Logger
+
+ alias Pleroma.Web.MediaProxy
+
+ @behaviour Plug
+ # no slashes
+ @path "media"
+
+ @default_cache_control_header "public, max-age=1209600"
+
+ def init(_opts) do
+ static_plug_opts =
+ [
+ headers: %{"cache-control" => @default_cache_control_header},
+ cache_control_for_etags: @default_cache_control_header
+ ]
+ |> Keyword.put(:from, "__unconfigured_media_plug")
+ |> Keyword.put(:at, "/__unconfigured_media_plug")
+ |> Plug.Static.init()
+
+ %{static_plug_opts: static_plug_opts}
+ end
+
+ def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do
+ conn =
+ case fetch_query_params(conn) do
+ %{query_params: %{"name" => name}} = conn ->
+ name = String.replace(name, "\"", "\\\"")
+
+ put_resp_header(conn, "content-disposition", "filename=\"#{name}\"")
+
+ conn ->
+ conn
+ end
+ |> merge_resp_headers([{"content-security-policy", "sandbox"}])
+
+ config = Pleroma.Config.get(Pleroma.Upload)
+
+ with uploader <- Keyword.fetch!(config, :uploader),
+ proxy_remote = Keyword.get(config, :proxy_remote, false),
+ {:ok, get_method} <- uploader.get_file(file),
+ false <- media_is_banned(conn, get_method) do
+ get_media(conn, get_method, proxy_remote, opts)
+ else
+ _ ->
+ conn
+ |> send_resp(:internal_server_error, dgettext("errors", "Failed"))
+ |> halt()
+ end
+ end
+
+ def call(conn, _opts), do: conn
+
+ defp media_is_banned(%{request_path: path} = _conn, {:static_dir, _}) do
+ MediaProxy.in_banned_urls(Pleroma.Web.base_url() <> path)
+ end
+
+ defp media_is_banned(_, {:url, url}), do: MediaProxy.in_banned_urls(url)
+
+ defp media_is_banned(_, _), do: false
+
+ defp get_media(conn, {:static_dir, directory}, _, opts) do
+ static_opts =
+ Map.get(opts, :static_plug_opts)
+ |> Map.put(:at, [@path])
+ |> Map.put(:from, directory)
+
+ conn = Plug.Static.call(conn, static_opts)
+
+ if conn.halted do
+ conn
+ else
+ conn
+ |> send_resp(:not_found, dgettext("errors", "Not found"))
+ |> halt()
+ end
+ end
+
+ defp get_media(conn, {:url, url}, true, _) do
+ conn
+ |> Pleroma.ReverseProxy.call(url, Pleroma.Config.get([Pleroma.Upload, :proxy_opts], []))
+ end
+
+ defp get_media(conn, {:url, url}, _, _) do
+ conn
+ |> Phoenix.Controller.redirect(external: url)
+ |> halt()
+ end
+
+ defp get_media(conn, unknown, _, _) do
+ Logger.error("#{__MODULE__}: Unknown get startegy: #{inspect(unknown)}")
+
+ conn
+ |> send_resp(:internal_server_error, dgettext("errors", "Internal Error"))
+ |> halt()
+ end
+end
diff --git a/lib/pleroma/web/plugs/user_enabled_plug.ex b/lib/pleroma/web/plugs/user_enabled_plug.ex
new file mode 100644
index 000000000..fa28ee48b
--- /dev/null
+++ b/lib/pleroma/web/plugs/user_enabled_plug.ex
@@ -0,0 +1,23 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.UserEnabledPlug do
+ import Plug.Conn
+ alias Pleroma.User
+
+ def init(options) do
+ options
+ end
+
+ def call(%{assigns: %{user: %User{} = user}} = conn, _) do
+ case User.account_status(user) do
+ :active -> conn
+ _ -> assign(conn, :user, nil)
+ end
+ end
+
+ def call(conn, _) do
+ conn
+ end
+end
diff --git a/lib/pleroma/web/plugs/user_fetcher_plug.ex b/lib/pleroma/web/plugs/user_fetcher_plug.ex
new file mode 100644
index 000000000..4039600da
--- /dev/null
+++ b/lib/pleroma/web/plugs/user_fetcher_plug.ex
@@ -0,0 +1,21 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.UserFetcherPlug do
+ alias Pleroma.User
+ import Plug.Conn
+
+ def init(options) do
+ options
+ end
+
+ def call(conn, _options) do
+ with %{auth_credentials: %{username: username}} <- conn.assigns,
+ %User{} = user <- User.get_by_nickname_or_email(username) do
+ assign(conn, :auth_user, user)
+ else
+ _ -> conn
+ end
+ end
+end
diff --git a/lib/pleroma/web/plugs/user_is_admin_plug.ex b/lib/pleroma/web/plugs/user_is_admin_plug.ex
new file mode 100644
index 000000000..531c965f0
--- /dev/null
+++ b/lib/pleroma/web/plugs/user_is_admin_plug.ex
@@ -0,0 +1,24 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.UserIsAdminPlug do
+ import Pleroma.Web.TranslationHelpers
+ import Plug.Conn
+
+ alias Pleroma.User
+
+ def init(options) do
+ options
+ end
+
+ def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _) do
+ conn
+ end
+
+ def call(conn, _) do
+ conn
+ |> render_error(:forbidden, "User is not an admin.")
+ |> halt()
+ end
+end