diff options
Diffstat (limited to 'lib/pleroma/plugs')
-rw-r--r-- | lib/pleroma/plugs/authentication_plug.ex | 15 | ||||
-rw-r--r-- | lib/pleroma/plugs/ensure_authenticated_plug.ex | 4 | ||||
-rw-r--r-- | lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex | 4 | ||||
-rw-r--r-- | lib/pleroma/plugs/federating_plug.ex | 2 | ||||
-rw-r--r-- | lib/pleroma/plugs/http_security_plug.ex | 4 | ||||
-rw-r--r-- | lib/pleroma/plugs/idempotency_plug.ex | 84 | ||||
-rw-r--r-- | lib/pleroma/plugs/oauth_scopes_plug.ex | 8 | ||||
-rw-r--r-- | lib/pleroma/plugs/rate_limit_plug.ex | 36 | ||||
-rw-r--r-- | lib/pleroma/plugs/rate_limiter.ex | 131 | ||||
-rw-r--r-- | lib/pleroma/plugs/set_locale_plug.ex | 63 | ||||
-rw-r--r-- | lib/pleroma/plugs/uploaded_media.ex | 9 | ||||
-rw-r--r-- | lib/pleroma/plugs/user_is_admin_plug.ex | 4 |
12 files changed, 313 insertions, 51 deletions
diff --git a/lib/pleroma/plugs/authentication_plug.ex b/lib/pleroma/plugs/authentication_plug.ex index da4ed4226..eec514892 100644 --- a/lib/pleroma/plugs/authentication_plug.ex +++ b/lib/pleroma/plugs/authentication_plug.ex @@ -6,11 +6,26 @@ defmodule Pleroma.Plugs.AuthenticationPlug do alias Comeonin.Pbkdf2 import Plug.Conn alias Pleroma.User + require Logger def init(options) do options end + def checkpw(password, password_hash) do + cond do + String.starts_with?(password_hash, "$pbkdf2") -> + Pbkdf2.checkpw(password, password_hash) + + String.starts_with?(password_hash, "$6") -> + :crypt.crypt(password, password_hash) == password_hash + + true -> + Logger.error("Password hash not recognized") + false + end + end + def call(%{assigns: %{user: %User{}}} = conn, _), do: conn def call( diff --git a/lib/pleroma/plugs/ensure_authenticated_plug.ex b/lib/pleroma/plugs/ensure_authenticated_plug.ex index 11c4342c4..27cd41aec 100644 --- a/lib/pleroma/plugs/ensure_authenticated_plug.ex +++ b/lib/pleroma/plugs/ensure_authenticated_plug.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Plugs.EnsureAuthenticatedPlug do import Plug.Conn + import Pleroma.Web.TranslationHelpers alias Pleroma.User def init(options) do @@ -16,8 +17,7 @@ defmodule Pleroma.Plugs.EnsureAuthenticatedPlug do def call(conn, _) do conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{error: "Invalid credentials."})) + |> 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 index 317fd5445..a16f61435 100644 --- a/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex +++ b/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex @@ -3,6 +3,7 @@ # 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 @@ -23,8 +24,7 @@ defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug do {false, _} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{error: "This resource requires authentication."})) + |> render_error(:forbidden, "This resource requires authentication.") |> halt end end diff --git a/lib/pleroma/plugs/federating_plug.ex b/lib/pleroma/plugs/federating_plug.ex index effc154bf..4dc4e9279 100644 --- a/lib/pleroma/plugs/federating_plug.ex +++ b/lib/pleroma/plugs/federating_plug.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Web.FederatingPlug do end def call(conn, _opts) do - if Keyword.get(Application.get_env(:pleroma, :instance), :federating) do + if Pleroma.Config.get([:instance, :federating]) do conn else conn diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index 485ddfbc7..a7cc22831 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -56,14 +56,14 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do connect_src = "connect-src 'self' #{static_url} #{websocket_url}" connect_src = - if Mix.env() == :dev do + if Pleroma.Config.get(:env) == :dev do connect_src <> " http://localhost:3035/" else connect_src end script_src = - if Mix.env() == :dev do + if Pleroma.Config.get(:env) == :dev do "script-src 'self' 'unsafe-eval'" else "script-src 'self'" diff --git a/lib/pleroma/plugs/idempotency_plug.ex b/lib/pleroma/plugs/idempotency_plug.ex new file mode 100644 index 000000000..e99c5d279 --- /dev/null +++ b/lib/pleroma/plugs/idempotency_plug.ex @@ -0,0 +1,84 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# 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/oauth_scopes_plug.ex b/lib/pleroma/plugs/oauth_scopes_plug.ex index f2bfa2b1a..b508628a9 100644 --- a/lib/pleroma/plugs/oauth_scopes_plug.ex +++ b/lib/pleroma/plugs/oauth_scopes_plug.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do import Plug.Conn + import Pleroma.Web.Gettext @behaviour Plug @@ -30,11 +31,14 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do true -> missing_scopes = scopes -- token.scopes - error_message = "Insufficient permissions: #{Enum.join(missing_scopes, " #{op} ")}." + permissions = Enum.join(missing_scopes, " #{op} ") + + error_message = + dgettext("errors", "Insufficient permissions: %{permissions}.", permissions: permissions) conn |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{error: error_message})) + |> send_resp(:forbidden, Jason.encode!(%{error: error_message})) |> halt() end end diff --git a/lib/pleroma/plugs/rate_limit_plug.ex b/lib/pleroma/plugs/rate_limit_plug.ex deleted file mode 100644 index 466f64a79..000000000 --- a/lib/pleroma/plugs/rate_limit_plug.ex +++ /dev/null @@ -1,36 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.RateLimitPlug do - import Phoenix.Controller, only: [json: 2] - import Plug.Conn - - def init(opts), do: opts - - def call(conn, opts) do - enabled? = Pleroma.Config.get([:app_account_creation, :enabled]) - - case check_rate(conn, Map.put(opts, :enabled, enabled?)) do - {:ok, _count} -> conn - {:error, _count} -> render_error(conn) - %Plug.Conn{} = conn -> conn - end - end - - defp check_rate(conn, %{enabled: true} = opts) do - max_requests = opts[:max_requests] - bucket_name = conn.remote_ip |> Tuple.to_list() |> Enum.join(".") - - ExRated.check_rate(bucket_name, opts[:interval] * 1000, max_requests) - end - - defp check_rate(conn, _), do: conn - - defp render_error(conn) do - conn - |> put_status(:forbidden) - |> json(%{error: "Rate limit exceeded."}) - |> halt() - end -end diff --git a/lib/pleroma/plugs/rate_limiter.ex b/lib/pleroma/plugs/rate_limiter.ex new file mode 100644 index 000000000..31388f574 --- /dev/null +++ b/lib/pleroma/plugs/rate_limiter.ex @@ -0,0 +1,131 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# 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, :limiter_name) + plug(Pleroma.Plugs.RateLimiter, {:limiter_name, options}) + + Allowed options: + + * `bucket_name` overrides bucket name (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, :one when action == :one) + plug(Pleroma.Plugs.RateLimiter, :two when action in [:two, :three]) + + plug( + Pleroma.Plugs.RateLimiter, + {: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, :one) + ... + end + """ + import Pleroma.Web.TranslationHelpers + import Plug.Conn + + alias Pleroma.User + + def init(limiter_name) when is_atom(limiter_name) do + init({limiter_name, []}) + end + + def init({limiter_name, opts}) do + case Pleroma.Config.get([:rate_limit, limiter_name]) do + nil -> nil + config -> {limiter_name, config, opts} + end + end + + # Do not limit if there is no limiter configuration + def call(conn, nil), do: conn + + def call(conn, settings) do + case check_rate(conn, settings) do + {:ok, _count} -> + conn + + {:error, _count} -> + render_throttled_error(conn) + end + end + + defp bucket_name(conn, limiter_name, opts) do + bucket_name = opts[:bucket_name] || limiter_name + + if params_names = opts[:params] do + params_values = for p <- Enum.sort(params_names), do: conn.params[p] + Enum.join([bucket_name] ++ params_values, ":") + else + bucket_name + end + end + + defp check_rate( + %{assigns: %{user: %User{id: user_id}}} = conn, + {limiter_name, [_, {scale, limit}], opts} + ) do + bucket_name = bucket_name(conn, limiter_name, opts) + ExRated.check_rate("#{bucket_name}:#{user_id}", scale, limit) + end + + defp check_rate(conn, {limiter_name, [{scale, limit} | _], opts}) do + bucket_name = bucket_name(conn, limiter_name, opts) + ExRated.check_rate("#{bucket_name}:#{ip(conn)}", scale, limit) + end + + defp check_rate(conn, {limiter_name, {scale, limit}, opts}) do + check_rate(conn, {limiter_name, [{scale, limit}, {scale, limit}], opts}) + end + + def 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 +end diff --git a/lib/pleroma/plugs/set_locale_plug.ex b/lib/pleroma/plugs/set_locale_plug.ex new file mode 100644 index 000000000..8646cb30d --- /dev/null +++ b/lib/pleroma/plugs/set_locale_plug.ex @@ -0,0 +1,63 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +# NOTE: this module is based on https://github.com/smeevil/set_locale +defmodule Pleroma.Plugs.SetLocalePlug do + import Plug.Conn, only: [get_req_header: 2, assign: 3] + + def init(_), do: nil + + def call(conn, _) do + locale = get_locale_from_header(conn) || Gettext.get_locale() + Gettext.put_locale(locale) + assign(conn, :locale, locale) + end + + defp get_locale_from_header(conn) do + conn + |> extract_accept_language() + |> Enum.find(&supported_locale?/1) + end + + defp extract_accept_language(conn) do + case get_req_header(conn, "accept-language") do + [value | _] -> + value + |> String.split(",") + |> Enum.map(&parse_language_option/1) + |> Enum.sort(&(&1.quality > &2.quality)) + |> Enum.map(& &1.tag) + |> Enum.reject(&is_nil/1) + |> ensure_language_fallbacks() + + _ -> + [] + end + end + + defp supported_locale?(locale) do + Pleroma.Web.Gettext + |> Gettext.known_locales() + |> Enum.member?(locale) + end + + defp parse_language_option(string) do + captures = Regex.named_captures(~r/^\s?(?<tag>[\w\-]+)(?:;q=(?<quality>[\d\.]+))?$/i, string) + + quality = + case Float.parse(captures["quality"] || "1.0") do + {val, _} -> val + :error -> 1.0 + end + + %{tag: captures["tag"], quality: quality} + end + + defp ensure_language_fallbacks(tags) do + Enum.flat_map(tags, fn tag -> + [language | _] = String.split(tag, "-") + if Enum.member?(tags, language), do: [tag], else: [tag, language] + end) + end +end diff --git a/lib/pleroma/plugs/uploaded_media.ex b/lib/pleroma/plugs/uploaded_media.ex index fd77b8d8f..69c1ab942 100644 --- a/lib/pleroma/plugs/uploaded_media.ex +++ b/lib/pleroma/plugs/uploaded_media.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Plugs.UploadedMedia do """ import Plug.Conn + import Pleroma.Web.Gettext require Logger @behaviour Plug @@ -36,7 +37,7 @@ defmodule Pleroma.Plugs.UploadedMedia do conn end - config = Pleroma.Config.get([Pleroma.Upload]) + config = Pleroma.Config.get(Pleroma.Upload) with uploader <- Keyword.fetch!(config, :uploader), proxy_remote = Keyword.get(config, :proxy_remote, false), @@ -45,7 +46,7 @@ defmodule Pleroma.Plugs.UploadedMedia do else _ -> conn - |> send_resp(500, "Failed") + |> send_resp(:internal_server_error, dgettext("errors", "Failed")) |> halt() end end @@ -64,7 +65,7 @@ defmodule Pleroma.Plugs.UploadedMedia do conn else conn - |> send_resp(404, "Not found") + |> send_resp(:not_found, dgettext("errors", "Not found")) |> halt() end end @@ -84,7 +85,7 @@ defmodule Pleroma.Plugs.UploadedMedia do Logger.error("#{__MODULE__}: Unknown get startegy: #{inspect(unknown)}") conn - |> send_resp(500, "Internal Error") + |> send_resp(:internal_server_error, dgettext("errors", "Internal Error")) |> halt() end end diff --git a/lib/pleroma/plugs/user_is_admin_plug.ex b/lib/pleroma/plugs/user_is_admin_plug.ex index 04329e919..4c4b3d610 100644 --- a/lib/pleroma/plugs/user_is_admin_plug.ex +++ b/lib/pleroma/plugs/user_is_admin_plug.ex @@ -3,6 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Plugs.UserIsAdminPlug do + import Pleroma.Web.TranslationHelpers import Plug.Conn alias Pleroma.User @@ -16,8 +17,7 @@ defmodule Pleroma.Plugs.UserIsAdminPlug do def call(conn, _) do conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{error: "User is not admin."})) + |> render_error(:forbidden, "User is not admin.") |> halt end end |