diff --git a/config/config.exs b/config/config.exs index d6d116314..b0814c302 100644 --- a/config/config.exs +++ b/config/config.exs @@ -686,6 +686,8 @@ config :pleroma, Pleroma.Web.Plugs.RemoteIp, "192.168.0.0/16" ] +config :pleroma, Pleroma.Web.Plugs.StoreUserIpPlug, enabled: false + config :pleroma, :static_fe, enabled: false # Example of frontend configuration diff --git a/config/description.exs b/config/description.exs index f438a88ab..dd329f7d7 100644 --- a/config/description.exs +++ b/config/description.exs @@ -2844,7 +2844,7 @@ config :pleroma, :config_description, [ %{ key: :enabled, type: :boolean, - description: "Enable/disable the plug. Default: disabled." + description: "Enable/disable the plug. Default: enabled." }, %{ key: :headers, @@ -2870,6 +2870,21 @@ config :pleroma, :config_description, [ }, %{ group: :pleroma, + key: Pleroma.Web.Plugs.StoreUserIpPlug, + type: :group, + description: """ + Stores the user's last known IP address in the database if enabled. IP addresses are shown in AdminAPI. + """, + children: [ + %{ + key: :enabled, + type: :boolean, + description: "Enable/disable the plug. Default: disabled." + } + ] + }, + %{ + group: :pleroma, key: :web_cache_ttl, label: "Web cache TTL", type: :group, diff --git a/lib/pleroma/ecto_type/ip_address.ex b/lib/pleroma/ecto_type/ip_address.ex new file mode 100644 index 000000000..5b9b5f50e --- /dev/null +++ b/lib/pleroma/ecto_type/ip_address.ex @@ -0,0 +1,22 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EctoType.IpAddress do + alias Postgrex.INET + @behaviour Ecto.Type + + def type, do: :inet + + def cast(%INET{address: ip, netmask: nil}), do: {:ok, ip} + def cast(ip) when is_tuple(ip), do: {:ok, ip} + + def load(%INET{address: ip, netmask: nil}), do: {:ok, ip} + + def dump(ip) when is_tuple(ip), do: {:ok, %INET{address: ip, netmask: nil}} + def dump(_), do: :error + + def equal?(a, b), do: a == b + + def embed_as(_), do: :self +end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 52730fd8d..382de29c5 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -146,6 +146,7 @@ defmodule Pleroma.User do field(:inbox, :string) field(:shared_inbox, :string) field(:accepts_chat_messages, :boolean, default: nil) + field(:last_known_ip, Pleroma.EctoType.IpAddress) embeds_one( :notification_settings, @@ -2457,4 +2458,12 @@ defmodule Pleroma.User do def get_host(%User{ap_id: ap_id} = _user) do URI.parse(ap_id).host end + + def update_last_known_ip(%User{last_known_ip: ip} = user, ip), do: {:ok, user} + + def update_last_known_ip(%User{} = user, ip) when is_tuple(ip) do + user + |> change(last_known_ip: ip) + |> update_and_set_cache() + end end diff --git a/lib/pleroma/web/plugs/store_user_ip_plug.ex b/lib/pleroma/web/plugs/store_user_ip_plug.ex new file mode 100644 index 000000000..5e2245a41 --- /dev/null +++ b/lib/pleroma/web/plugs/store_user_ip_plug.ex @@ -0,0 +1,39 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.StoreUserIpPlug do + @moduledoc """ + Stores the user's last known IP address in the database if enabled. + User IP addresses are shown in AdminAPI. + """ + + alias Pleroma.Config + alias Pleroma.User + import Plug.Conn + + @behaviour Plug + + def init(_), do: nil + + # IP address hasn't changed, so skip + def call( + %{remote_ip: ip, assigns: %{remote_ip_found: true, user: %User{last_known_ip: ip}}} = + conn, + _ + ), + do: conn + + # Store user IP if enabled + def call(%{remote_ip: ip, assigns: %{remote_ip_found: true, user: %User{} = user}} = conn, _) do + with true <- Config.get([__MODULE__, :enabled]), + {:ok, %User{} = user} <- User.update_last_known_ip(user, ip) do + assign(conn, :user, user) + else + _ -> conn + end + end + + # Fail silently + def call(conn, _), do: conn +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index aefc9f0be..b8565b6dd 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -56,6 +56,7 @@ defmodule Pleroma.Web.Router do plug(Pleroma.Web.Plugs.UserEnabledPlug) plug(Pleroma.Web.Plugs.SetUserSessionIdPlug) plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug) + plug(Pleroma.Web.Plugs.StoreUserIpPlug) end pipeline :base_api do diff --git a/priv/repo/migrations/20210106181804_add_last_known_ip_to_users.exs b/priv/repo/migrations/20210106181804_add_last_known_ip_to_users.exs new file mode 100644 index 000000000..e068892e6 --- /dev/null +++ b/priv/repo/migrations/20210106181804_add_last_known_ip_to_users.exs @@ -0,0 +1,11 @@ +defmodule Pleroma.Repo.Migrations.AddLastKnownIpToUsers do + use Ecto.Migration + + def change do + alter table(:users) do + add(:last_known_ip, :inet) + end + + create(index(:users, [:last_known_ip])) + end +end diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index 40bbcad0b..1e487a25b 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -2271,4 +2271,11 @@ defmodule Pleroma.UserTest do user = insert(:user, ap_id: "https://lain.com/users/lain", nickname: "lain") assert User.get_host(user) == "lain.com" end + + test "update_last_known_ip/2" do + %User{id: user_id} = user = insert(:user, last_known_ip: {1, 2, 3, 4}) + + assert {:ok, %User{id: ^user_id, last_known_ip: {5, 4, 3, 2}}} = + User.update_last_known_ip(user, {5, 4, 3, 2}) + end end diff --git a/test/pleroma/web/plugs/store_user_ip_plug_test.exs b/test/pleroma/web/plugs/store_user_ip_plug_test.exs new file mode 100644 index 000000000..9727a53e0 --- /dev/null +++ b/test/pleroma/web/plugs/store_user_ip_plug_test.exs @@ -0,0 +1,59 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.StoreUserIpPlugTest do + use Pleroma.Web.ConnCase, async: true + use Plug.Test + alias Pleroma.User + alias Pleroma.Web.Plugs.RemoteIp + alias Pleroma.Web.Plugs.StoreUserIpPlug + import Pleroma.Factory + + setup do: clear_config(StoreUserIpPlug, enabled: true) + + setup do: + clear_config(RemoteIp, + enabled: true, + headers: ["x-forwarded-for"], + proxies: [], + reserved: [ + "127.0.0.0/8", + "::1/128", + "fc00::/7", + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16" + ] + ) + + test "stores the user's IP address", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> put_req_header("x-forwarded-for", "1.2.3.4") + |> RemoteIp.call(nil) + |> StoreUserIpPlug.call(nil) + + user = User.get_by_id(user.id) + assert user.last_known_ip == {1, 2, 3, 4} + assert %Plug.Conn{assigns: %{user: %User{last_known_ip: {1, 2, 3, 4}} = ^user}} = conn + end + + test "does nothing when disabled", %{conn: conn} do + clear_config(StoreUserIpPlug, enabled: false) + user = insert(:user, last_known_ip: {1, 2, 3, 4}) + + conn = + conn + |> assign(:user, user) + |> put_req_header("x-forwarded-for", "5.4.3.2") + |> RemoteIp.call(nil) + |> StoreUserIpPlug.call(nil) + + assert user == User.get_by_id(user.id) + assert %Plug.Conn{assigns: %{user: %User{last_known_ip: {1, 2, 3, 4}} = ^user}} = conn + end +end