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