From 2501793f819813d792fdae493fb0f1e65c6cc7b3 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 24 Jun 2020 08:35:22 +0300 Subject: moving plugs into web dir --- .../plugs/admin_secret_authentication_plug.ex | 60 ----- lib/pleroma/plugs/authentication_plug.ex | 80 ------ lib/pleroma/plugs/basic_auth_decoder_plug.ex | 25 -- lib/pleroma/plugs/cache.ex | 136 ----------- lib/pleroma/plugs/digest.ex | 14 -- lib/pleroma/plugs/ensure_authenticated_plug.ex | 41 ---- .../plugs/ensure_public_or_authenticated_plug.ex | 35 --- lib/pleroma/plugs/ensure_user_key_plug.ex | 18 -- .../plugs/expect_authenticated_check_plug.ex | 20 -- .../expect_public_or_authenticated_check_plug.ex | 21 -- lib/pleroma/plugs/federating_plug.ex | 32 --- lib/pleroma/plugs/http_security_plug.ex | 225 ----------------- lib/pleroma/plugs/http_signature.ex | 65 ----- lib/pleroma/plugs/idempotency_plug.ex | 84 ------- lib/pleroma/plugs/instance_static.ex | 53 ---- lib/pleroma/plugs/legacy_authentication_plug.ex | 42 ---- .../plugs/mapped_signature_to_identity_plug.ex | 71 ------ lib/pleroma/plugs/o_auth_plug.ex | 120 --------- lib/pleroma/plugs/o_auth_scopes_plug.ex | 77 ------ lib/pleroma/plugs/plug_helper.ex | 40 --- .../plugs/rate_limiter/limiter_supervisor.ex | 54 ----- lib/pleroma/plugs/rate_limiter/rate_limiter.ex | 267 --------------------- lib/pleroma/plugs/rate_limiter/supervisor.ex | 20 -- lib/pleroma/plugs/remote_ip.ex | 48 ---- lib/pleroma/plugs/session_authentication_plug.ex | 21 -- lib/pleroma/plugs/set_format_plug.ex | 24 -- lib/pleroma/plugs/set_locale_plug.ex | 63 ----- lib/pleroma/plugs/set_user_session_id_plug.ex | 19 -- lib/pleroma/plugs/static_fe_plug.ex | 26 -- lib/pleroma/plugs/trailing_format_plug.ex | 42 ---- lib/pleroma/plugs/uploaded_media.ex | 107 --------- lib/pleroma/plugs/user_enabled_plug.ex | 23 -- lib/pleroma/plugs/user_fetcher_plug.ex | 21 -- lib/pleroma/plugs/user_is_admin_plug.ex | 24 -- .../web/plugs/admin_secret_authentication_plug.ex | 60 +++++ lib/pleroma/web/plugs/authentication_plug.ex | 80 ++++++ lib/pleroma/web/plugs/basic_auth_decoder_plug.ex | 25 ++ lib/pleroma/web/plugs/cache.ex | 136 +++++++++++ lib/pleroma/web/plugs/digest.ex | 14 ++ lib/pleroma/web/plugs/ensure_authenticated_plug.ex | 41 ++++ .../plugs/ensure_public_or_authenticated_plug.ex | 35 +++ lib/pleroma/web/plugs/ensure_user_key_plug.ex | 18 ++ .../web/plugs/expect_authenticated_check_plug.ex | 20 ++ .../expect_public_or_authenticated_check_plug.ex | 21 ++ lib/pleroma/web/plugs/federating_plug.ex | 32 +++ lib/pleroma/web/plugs/http_security_plug.ex | 225 +++++++++++++++++ lib/pleroma/web/plugs/http_signature.ex | 65 +++++ lib/pleroma/web/plugs/idempotency_plug.ex | 84 +++++++ lib/pleroma/web/plugs/instance_static.ex | 53 ++++ .../web/plugs/legacy_authentication_plug.ex | 42 ++++ .../web/plugs/mapped_signature_to_identity_plug.ex | 71 ++++++ lib/pleroma/web/plugs/o_auth_plug.ex | 120 +++++++++ lib/pleroma/web/plugs/o_auth_scopes_plug.ex | 77 ++++++ lib/pleroma/web/plugs/plug_helper.ex | 40 +++ lib/pleroma/web/plugs/rate_limiter.ex | 267 +++++++++++++++++++++ .../web/plugs/rate_limiter/limiter_supervisor.ex | 54 +++++ lib/pleroma/web/plugs/rate_limiter/supervisor.ex | 20 ++ lib/pleroma/web/plugs/remote_ip.ex | 48 ++++ .../web/plugs/session_authentication_plug.ex | 21 ++ lib/pleroma/web/plugs/set_format_plug.ex | 24 ++ lib/pleroma/web/plugs/set_locale_plug.ex | 63 +++++ lib/pleroma/web/plugs/set_user_session_id_plug.ex | 19 ++ lib/pleroma/web/plugs/static_fe_plug.ex | 26 ++ lib/pleroma/web/plugs/trailing_format_plug.ex | 42 ++++ lib/pleroma/web/plugs/uploaded_media.ex | 107 +++++++++ lib/pleroma/web/plugs/user_enabled_plug.ex | 23 ++ lib/pleroma/web/plugs/user_fetcher_plug.ex | 21 ++ lib/pleroma/web/plugs/user_is_admin_plug.ex | 24 ++ 68 files changed, 2018 insertions(+), 2018 deletions(-) delete mode 100644 lib/pleroma/plugs/admin_secret_authentication_plug.ex delete mode 100644 lib/pleroma/plugs/authentication_plug.ex delete mode 100644 lib/pleroma/plugs/basic_auth_decoder_plug.ex delete mode 100644 lib/pleroma/plugs/cache.ex delete mode 100644 lib/pleroma/plugs/digest.ex delete mode 100644 lib/pleroma/plugs/ensure_authenticated_plug.ex delete mode 100644 lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex delete mode 100644 lib/pleroma/plugs/ensure_user_key_plug.ex delete mode 100644 lib/pleroma/plugs/expect_authenticated_check_plug.ex delete mode 100644 lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex delete mode 100644 lib/pleroma/plugs/federating_plug.ex delete mode 100644 lib/pleroma/plugs/http_security_plug.ex delete mode 100644 lib/pleroma/plugs/http_signature.ex delete mode 100644 lib/pleroma/plugs/idempotency_plug.ex delete mode 100644 lib/pleroma/plugs/instance_static.ex delete mode 100644 lib/pleroma/plugs/legacy_authentication_plug.ex delete mode 100644 lib/pleroma/plugs/mapped_signature_to_identity_plug.ex delete mode 100644 lib/pleroma/plugs/o_auth_plug.ex delete mode 100644 lib/pleroma/plugs/o_auth_scopes_plug.ex delete mode 100644 lib/pleroma/plugs/plug_helper.ex delete mode 100644 lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex delete mode 100644 lib/pleroma/plugs/rate_limiter/rate_limiter.ex delete mode 100644 lib/pleroma/plugs/rate_limiter/supervisor.ex delete mode 100644 lib/pleroma/plugs/remote_ip.ex delete mode 100644 lib/pleroma/plugs/session_authentication_plug.ex delete mode 100644 lib/pleroma/plugs/set_format_plug.ex delete mode 100644 lib/pleroma/plugs/set_locale_plug.ex delete mode 100644 lib/pleroma/plugs/set_user_session_id_plug.ex delete mode 100644 lib/pleroma/plugs/static_fe_plug.ex delete mode 100644 lib/pleroma/plugs/trailing_format_plug.ex delete mode 100644 lib/pleroma/plugs/uploaded_media.ex delete mode 100644 lib/pleroma/plugs/user_enabled_plug.ex delete mode 100644 lib/pleroma/plugs/user_fetcher_plug.ex delete mode 100644 lib/pleroma/plugs/user_is_admin_plug.ex create mode 100644 lib/pleroma/web/plugs/admin_secret_authentication_plug.ex create mode 100644 lib/pleroma/web/plugs/authentication_plug.ex create mode 100644 lib/pleroma/web/plugs/basic_auth_decoder_plug.ex create mode 100644 lib/pleroma/web/plugs/cache.ex create mode 100644 lib/pleroma/web/plugs/digest.ex create mode 100644 lib/pleroma/web/plugs/ensure_authenticated_plug.ex create mode 100644 lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex create mode 100644 lib/pleroma/web/plugs/ensure_user_key_plug.ex create mode 100644 lib/pleroma/web/plugs/expect_authenticated_check_plug.ex create mode 100644 lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex create mode 100644 lib/pleroma/web/plugs/federating_plug.ex create mode 100644 lib/pleroma/web/plugs/http_security_plug.ex create mode 100644 lib/pleroma/web/plugs/http_signature.ex create mode 100644 lib/pleroma/web/plugs/idempotency_plug.ex create mode 100644 lib/pleroma/web/plugs/instance_static.ex create mode 100644 lib/pleroma/web/plugs/legacy_authentication_plug.ex create mode 100644 lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex create mode 100644 lib/pleroma/web/plugs/o_auth_plug.ex create mode 100644 lib/pleroma/web/plugs/o_auth_scopes_plug.ex create mode 100644 lib/pleroma/web/plugs/plug_helper.ex create mode 100644 lib/pleroma/web/plugs/rate_limiter.ex create mode 100644 lib/pleroma/web/plugs/rate_limiter/limiter_supervisor.ex create mode 100644 lib/pleroma/web/plugs/rate_limiter/supervisor.ex create mode 100644 lib/pleroma/web/plugs/remote_ip.ex create mode 100644 lib/pleroma/web/plugs/session_authentication_plug.ex create mode 100644 lib/pleroma/web/plugs/set_format_plug.ex create mode 100644 lib/pleroma/web/plugs/set_locale_plug.ex create mode 100644 lib/pleroma/web/plugs/set_user_session_id_plug.ex create mode 100644 lib/pleroma/web/plugs/static_fe_plug.ex create mode 100644 lib/pleroma/web/plugs/trailing_format_plug.ex create mode 100644 lib/pleroma/web/plugs/uploaded_media.ex create mode 100644 lib/pleroma/web/plugs/user_enabled_plug.ex create mode 100644 lib/pleroma/web/plugs/user_fetcher_plug.ex create mode 100644 lib/pleroma/web/plugs/user_is_admin_plug.ex diff --git a/lib/pleroma/plugs/admin_secret_authentication_plug.ex b/lib/pleroma/plugs/admin_secret_authentication_plug.ex deleted file mode 100644 index 2e54df47a..000000000 --- a/lib/pleroma/plugs/admin_secret_authentication_plug.ex +++ /dev/null @@ -1,60 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.AdminSecretAuthenticationPlug do - import Plug.Conn - - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.RateLimiter - alias Pleroma.User - - 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/plugs/authentication_plug.ex b/lib/pleroma/plugs/authentication_plug.ex deleted file mode 100644 index 057ea42f1..000000000 --- a/lib/pleroma/plugs/authentication_plug.ex +++ /dev/null @@ -1,80 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.AuthenticationPlug do - alias Pleroma.Plugs.OAuthScopesPlug - 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) - |> 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/plugs/basic_auth_decoder_plug.ex b/lib/pleroma/plugs/basic_auth_decoder_plug.ex deleted file mode 100644 index af7ecb0d8..000000000 --- a/lib/pleroma/plugs/basic_auth_decoder_plug.ex +++ /dev/null @@ -1,25 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.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/plugs/cache.ex b/lib/pleroma/plugs/cache.ex deleted file mode 100644 index f65c2a189..000000000 --- a/lib/pleroma/plugs/cache.ex +++ /dev/null @@ -1,136 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.Cache do - @moduledoc """ - Caches successful GET responses. - - To enable the cache add the plug to a router pipeline or controller: - - plug(Pleroma.Plugs.Cache) - - ## Configuration - - To configure the plug you need to pass settings as the second argument to the `plug/2` macro: - - plug(Pleroma.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/plugs/digest.ex b/lib/pleroma/plugs/digest.ex deleted file mode 100644 index b521b3073..000000000 --- a/lib/pleroma/plugs/digest.ex +++ /dev/null @@ -1,14 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# 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/plugs/ensure_authenticated_plug.ex b/lib/pleroma/plugs/ensure_authenticated_plug.ex deleted file mode 100644 index 3fe550806..000000000 --- a/lib/pleroma/plugs/ensure_authenticated_plug.ex +++ /dev/null @@ -1,41 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.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/plugs/ensure_public_or_authenticated_plug.ex b/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex deleted file mode 100644 index 7265bb87a..000000000 --- a/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex +++ /dev/null @@ -1,35 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.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/plugs/ensure_user_key_plug.ex b/lib/pleroma/plugs/ensure_user_key_plug.ex deleted file mode 100644 index 9795cdbde..000000000 --- a/lib/pleroma/plugs/ensure_user_key_plug.ex +++ /dev/null @@ -1,18 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.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/plugs/expect_authenticated_check_plug.ex b/lib/pleroma/plugs/expect_authenticated_check_plug.ex deleted file mode 100644 index 66b8d5de5..000000000 --- a/lib/pleroma/plugs/expect_authenticated_check_plug.ex +++ /dev/null @@ -1,20 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.ExpectAuthenticatedCheckPlug do - @moduledoc """ - Marks `Pleroma.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/plugs/expect_public_or_authenticated_check_plug.ex b/lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex deleted file mode 100644 index ba0ef76bd..000000000 --- a/lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex +++ /dev/null @@ -1,21 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug do - @moduledoc """ - Marks `Pleroma.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/plugs/federating_plug.ex b/lib/pleroma/plugs/federating_plug.ex deleted file mode 100644 index 09038f3c6..000000000 --- a/lib/pleroma/plugs/federating_plug.ex +++ /dev/null @@ -1,32 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.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/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex deleted file mode 100644 index c363b193b..000000000 --- a/lib/pleroma/plugs/http_security_plug.ex +++ /dev/null @@ -1,225 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.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/plugs/http_signature.ex b/lib/pleroma/plugs/http_signature.ex deleted file mode 100644 index 036e2a773..000000000 --- a/lib/pleroma/plugs/http_signature.ex +++ /dev/null @@ -1,65 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# 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/plugs/idempotency_plug.ex b/lib/pleroma/plugs/idempotency_plug.ex deleted file mode 100644 index f41397075..000000000 --- a/lib/pleroma/plugs/idempotency_plug.ex +++ /dev/null @@ -1,84 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.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/plugs/instance_static.ex b/lib/pleroma/plugs/instance_static.ex deleted file mode 100644 index 0fb57e422..000000000 --- a/lib/pleroma/plugs/instance_static.ex +++ /dev/null @@ -1,53 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.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.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/plugs/legacy_authentication_plug.ex b/lib/pleroma/plugs/legacy_authentication_plug.ex deleted file mode 100644 index d346e01a6..000000000 --- a/lib/pleroma/plugs/legacy_authentication_plug.ex +++ /dev/null @@ -1,42 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.LegacyAuthenticationPlug do - import Plug.Conn - - alias Pleroma.Plugs.OAuthScopesPlug - 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) - |> OAuthScopesPlug.skip_plug() - else - _ -> - conn - end - end - - def call(conn, _) do - conn - end -end diff --git a/lib/pleroma/plugs/mapped_signature_to_identity_plug.ex b/lib/pleroma/plugs/mapped_signature_to_identity_plug.ex deleted file mode 100644 index f44d4dee5..000000000 --- a/lib/pleroma/plugs/mapped_signature_to_identity_plug.ex +++ /dev/null @@ -1,71 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# 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/plugs/o_auth_plug.ex b/lib/pleroma/plugs/o_auth_plug.ex deleted file mode 100644 index 6fa71ef47..000000000 --- a/lib/pleroma/plugs/o_auth_plug.ex +++ /dev/null @@ -1,120 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.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/plugs/o_auth_scopes_plug.ex b/lib/pleroma/plugs/o_auth_scopes_plug.ex deleted file mode 100644 index b1a736d78..000000000 --- a/lib/pleroma/plugs/o_auth_scopes_plug.ex +++ /dev/null @@ -1,77 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.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/plugs/plug_helper.ex b/lib/pleroma/plugs/plug_helper.ex deleted file mode 100644 index 9c67be8ef..000000000 --- a/lib/pleroma/plugs/plug_helper.ex +++ /dev/null @@ -1,40 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.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/plugs/rate_limiter/limiter_supervisor.ex b/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex deleted file mode 100644 index 0bf5aadfb..000000000 --- a/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex +++ /dev/null @@ -1,54 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.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/plugs/rate_limiter/rate_limiter.ex b/lib/pleroma/plugs/rate_limiter/rate_limiter.ex deleted file mode 100644 index c51e2c634..000000000 --- a/lib/pleroma/plugs/rate_limiter/rate_limiter.ex +++ /dev/null @@ -1,267 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.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.Plugs.RateLimiter, name: :limiter_name) - plug(Pleroma.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.Plugs.RateLimiter, [name: :one] when action == :one) - plug(Pleroma.Plugs.RateLimiter, [name: :two] when action in [:two, :three]) - - plug( - Pleroma.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.Plugs.RateLimiter, name: :one) - ... - end - """ - import Pleroma.Web.TranslationHelpers - import Plug.Conn - - alias Pleroma.Config - alias Pleroma.Plugs.RateLimiter.LimiterSupervisor - alias Pleroma.User - - 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/plugs/rate_limiter/supervisor.ex b/lib/pleroma/plugs/rate_limiter/supervisor.ex deleted file mode 100644 index ce196df52..000000000 --- a/lib/pleroma/plugs/rate_limiter/supervisor.ex +++ /dev/null @@ -1,20 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.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.Plugs.RateLimiter.LimiterSupervisor - ] - - opts = [strategy: :one_for_one, name: Pleroma.Web.Streamer.Supervisor] - Supervisor.init(children, opts) - end -end diff --git a/lib/pleroma/plugs/remote_ip.ex b/lib/pleroma/plugs/remote_ip.ex deleted file mode 100644 index 987022156..000000000 --- a/lib/pleroma/plugs/remote_ip.ex +++ /dev/null @@ -1,48 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.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/plugs/session_authentication_plug.ex b/lib/pleroma/plugs/session_authentication_plug.ex deleted file mode 100644 index 0f83a5e53..000000000 --- a/lib/pleroma/plugs/session_authentication_plug.ex +++ /dev/null @@ -1,21 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.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/plugs/set_format_plug.ex b/lib/pleroma/plugs/set_format_plug.ex deleted file mode 100644 index c03fcb28d..000000000 --- a/lib/pleroma/plugs/set_format_plug.ex +++ /dev/null @@ -1,24 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.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/plugs/set_locale_plug.ex b/lib/pleroma/plugs/set_locale_plug.ex deleted file mode 100644 index 9a21d0a9d..000000000 --- a/lib/pleroma/plugs/set_locale_plug.ex +++ /dev/null @@ -1,63 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -# NOTE: this module is based on https://github.com/smeevil/set_locale -defmodule Pleroma.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?(?[\w\-]+)(?:;q=(?[\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/plugs/set_user_session_id_plug.ex b/lib/pleroma/plugs/set_user_session_id_plug.ex deleted file mode 100644 index 730c4ac74..000000000 --- a/lib/pleroma/plugs/set_user_session_id_plug.ex +++ /dev/null @@ -1,19 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.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/plugs/static_fe_plug.ex b/lib/pleroma/plugs/static_fe_plug.ex deleted file mode 100644 index 143665c71..000000000 --- a/lib/pleroma/plugs/static_fe_plug.ex +++ /dev/null @@ -1,26 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.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/plugs/trailing_format_plug.ex b/lib/pleroma/plugs/trailing_format_plug.ex deleted file mode 100644 index 8b4d5fc9f..000000000 --- a/lib/pleroma/plugs/trailing_format_plug.ex +++ /dev/null @@ -1,42 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.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/plugs/uploaded_media.ex b/lib/pleroma/plugs/uploaded_media.ex deleted file mode 100644 index 40984cfc0..000000000 --- a/lib/pleroma/plugs/uploaded_media.ex +++ /dev/null @@ -1,107 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.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/plugs/user_enabled_plug.ex b/lib/pleroma/plugs/user_enabled_plug.ex deleted file mode 100644 index 23e800a74..000000000 --- a/lib/pleroma/plugs/user_enabled_plug.ex +++ /dev/null @@ -1,23 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.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/plugs/user_fetcher_plug.ex b/lib/pleroma/plugs/user_fetcher_plug.ex deleted file mode 100644 index 235c77d85..000000000 --- a/lib/pleroma/plugs/user_fetcher_plug.ex +++ /dev/null @@ -1,21 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.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/plugs/user_is_admin_plug.ex b/lib/pleroma/plugs/user_is_admin_plug.ex deleted file mode 100644 index 488a61d1d..000000000 --- a/lib/pleroma/plugs/user_is_admin_plug.ex +++ /dev/null @@ -1,24 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.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 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..2e54df47a --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.AdminSecretAuthenticationPlug do + import Plug.Conn + + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Plugs.RateLimiter + alias Pleroma.User + + 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..057ea42f1 --- /dev/null +++ b/lib/pleroma/web/plugs/authentication_plug.ex @@ -0,0 +1,80 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.AuthenticationPlug do + alias Pleroma.Plugs.OAuthScopesPlug + 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) + |> 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..af7ecb0d8 --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.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..f65c2a189 --- /dev/null +++ b/lib/pleroma/web/plugs/cache.ex @@ -0,0 +1,136 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.Cache do + @moduledoc """ + Caches successful GET responses. + + To enable the cache add the plug to a router pipeline or controller: + + plug(Pleroma.Plugs.Cache) + + ## Configuration + + To configure the plug you need to pass settings as the second argument to the `plug/2` macro: + + plug(Pleroma.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.ex b/lib/pleroma/web/plugs/digest.ex new file mode 100644 index 000000000..b521b3073 --- /dev/null +++ b/lib/pleroma/web/plugs/digest.ex @@ -0,0 +1,14 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# 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..3fe550806 --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.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..7265bb87a --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.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..9795cdbde --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.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..66b8d5de5 --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.ExpectAuthenticatedCheckPlug do + @moduledoc """ + Marks `Pleroma.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..ba0ef76bd --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug do + @moduledoc """ + Marks `Pleroma.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..09038f3c6 --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.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/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex new file mode 100644 index 000000000..c363b193b --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.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.ex b/lib/pleroma/web/plugs/http_signature.ex new file mode 100644 index 000000000..036e2a773 --- /dev/null +++ b/lib/pleroma/web/plugs/http_signature.ex @@ -0,0 +1,65 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# 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..f41397075 --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.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..0fb57e422 --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.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.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..d346e01a6 --- /dev/null +++ b/lib/pleroma/web/plugs/legacy_authentication_plug.ex @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.LegacyAuthenticationPlug do + import Plug.Conn + + alias Pleroma.Plugs.OAuthScopesPlug + 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) + |> 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 +# 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..6fa71ef47 --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.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..b1a736d78 --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.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..9c67be8ef --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.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..c51e2c634 --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.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.Plugs.RateLimiter, name: :limiter_name) + plug(Pleroma.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.Plugs.RateLimiter, [name: :one] when action == :one) + plug(Pleroma.Plugs.RateLimiter, [name: :two] when action in [:two, :three]) + + plug( + Pleroma.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.Plugs.RateLimiter, name: :one) + ... + end + """ + import Pleroma.Web.TranslationHelpers + import Plug.Conn + + alias Pleroma.Config + alias Pleroma.Plugs.RateLimiter.LimiterSupervisor + alias Pleroma.User + + 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..0bf5aadfb --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.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..ce196df52 --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.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.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..987022156 --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.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..0f83a5e53 --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.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..c03fcb28d --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.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..9a21d0a9d --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +# NOTE: this module is based on https://github.com/smeevil/set_locale +defmodule Pleroma.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?(?[\w\-]+)(?:;q=(?[\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..730c4ac74 --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.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..143665c71 --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.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..8b4d5fc9f --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.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..40984cfc0 --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.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..23e800a74 --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.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..235c77d85 --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.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..488a61d1d --- /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 +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.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 -- cgit v1.2.3