mirror of
https://akkoma.dev/AkkomaGang/akkoma.git
synced 2024-12-18 15:58:30 +00:00
Check that the signature matches the creator
This commit is contained in:
parent
c6e63aaf6b
commit
03662501c3
|
@ -108,8 +108,8 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
||||||
Config.get([:mrf_simple, :reject], [])
|
Config.get([:mrf_simple, :reject], [])
|
||||||
end
|
end
|
||||||
|
|
||||||
defp should_federate?(inbox) do
|
def should_federate?(url) do
|
||||||
%{host: host} = URI.parse(inbox)
|
%{host: host} = URI.parse(url)
|
||||||
|
|
||||||
quarantined_instances =
|
quarantined_instances =
|
||||||
blocked_instances()
|
blocked_instances()
|
||||||
|
|
|
@ -19,6 +19,7 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
|
||||||
def call(%{assigns: %{valid_signature: true}, params: %{"actor" => actor}} = conn, _opts) do
|
def call(%{assigns: %{valid_signature: true}, params: %{"actor" => actor}} = conn, _opts) do
|
||||||
with actor_id <- Utils.get_ap_id(actor),
|
with actor_id <- Utils.get_ap_id(actor),
|
||||||
{:user, %User{} = user} <- {:user, user_from_key_id(conn)},
|
{:user, %User{} = user} <- {:user, user_from_key_id(conn)},
|
||||||
|
{:federate, true} <- {:federate, should_federate?(user)},
|
||||||
{:user_match, true} <- {:user_match, user.ap_id == actor_id} do
|
{:user_match, true} <- {:user_match, user.ap_id == actor_id} do
|
||||||
conn
|
conn
|
||||||
|> assign(:user, user)
|
|> assign(:user, user)
|
||||||
|
@ -27,33 +28,70 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
|
||||||
{:user_match, false} ->
|
{:user_match, false} ->
|
||||||
Logger.debug("Failed to map identity from signature (payload actor mismatch)")
|
Logger.debug("Failed to map identity from signature (payload actor mismatch)")
|
||||||
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{inspect(actor)}")
|
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{inspect(actor)}")
|
||||||
assign(conn, :valid_signature, false)
|
|
||||||
|
conn
|
||||||
|
|> assign(:valid_signature, false)
|
||||||
|
|
||||||
# remove me once testsuite uses mapped capabilities instead of what we do now
|
# remove me once testsuite uses mapped capabilities instead of what we do now
|
||||||
{:user, nil} ->
|
{:user, nil} ->
|
||||||
Logger.debug("Failed to map identity from signature (lookup failure)")
|
Logger.debug("Failed to map identity from signature (lookup failure)")
|
||||||
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}")
|
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}")
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|
|> assign(:valid_signature, false)
|
||||||
|
|
||||||
|
{:federate, false} ->
|
||||||
|
Logger.debug("Identity from signature is instance blocked")
|
||||||
|
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}")
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> assign(:valid_signature, false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# no payload, probably a signed fetch
|
# no payload, probably a signed fetch
|
||||||
def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
|
def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
|
||||||
with %User{} = user <- user_from_key_id(conn) do
|
with %User{} = user <- user_from_key_id(conn),
|
||||||
|
{:federate, true} <- {:federate, should_federate?(user)} do
|
||||||
conn
|
conn
|
||||||
|> assign(:user, user)
|
|> assign(:user, user)
|
||||||
|> AuthHelper.skip_oauth()
|
|> AuthHelper.skip_oauth()
|
||||||
else
|
else
|
||||||
|
{:federate, false} ->
|
||||||
|
Logger.debug("Identity from signature is instance blocked")
|
||||||
|
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}")
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> assign(:valid_signature, false)
|
||||||
|
|
||||||
|
nil ->
|
||||||
|
Logger.debug("Failed to map identity from signature (lookup failure)")
|
||||||
|
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}")
|
||||||
|
|
||||||
|
only_permit_user_routes(conn)
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
Logger.debug("Failed to map identity from signature (no payload actor mismatch)")
|
Logger.debug("Failed to map identity from signature (no payload actor mismatch)")
|
||||||
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}")
|
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}")
|
||||||
assign(conn, :valid_signature, false)
|
|
||||||
|
conn
|
||||||
|
|> assign(:valid_signature, false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# no signature at all
|
# no signature at all
|
||||||
def call(conn, _opts), do: conn
|
def call(conn, _opts), do: conn
|
||||||
|
|
||||||
|
defp only_permit_user_routes(%{path_info: ["users", _]} = conn) do
|
||||||
|
conn
|
||||||
|
|> assign(:limited_ap, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp only_permit_user_routes(conn) do
|
||||||
|
conn
|
||||||
|
|> assign(:valid_signature, false)
|
||||||
|
end
|
||||||
|
|
||||||
defp key_id_from_conn(conn) do
|
defp key_id_from_conn(conn) do
|
||||||
with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn),
|
with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn),
|
||||||
{:ok, ap_id} <- Signature.key_id_to_actor_id(key_id) do
|
{:ok, ap_id} <- Signature.key_id_to_actor_id(key_id) do
|
||||||
|
@ -73,4 +111,14 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp should_federate?(%User{ap_id: ap_id}), do: should_federate?(ap_id)
|
||||||
|
|
||||||
|
defp should_federate?(ap_id) do
|
||||||
|
if Pleroma.Config.get([:activitypub, :authorized_fetch_mode], false) do
|
||||||
|
Pleroma.Web.ActivityPub.Publisher.should_federate?(ap_id)
|
||||||
|
else
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -559,6 +559,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign(:valid_signature, true)
|
||||||
|
|> put_req_header(
|
||||||
|
"signature",
|
||||||
|
"keyId=\"http://mastodon.example.org/users/admin/main-key\""
|
||||||
|
)
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/inbox", data)
|
|> post("/inbox", data)
|
||||||
|
|
||||||
|
@ -589,6 +593,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign(:valid_signature, true)
|
||||||
|
|> put_req_header("signature", "keyId=\"#{user.ap_id}/main-key\"")
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/inbox", data)
|
|> post("/inbox", data)
|
||||||
|
|
||||||
|
@ -602,12 +607,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
|
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
|
||||||
|
|
||||||
sender_url = data["actor"]
|
sender_url = data["actor"]
|
||||||
|
sender = insert(:user, ap_id: data["actor"])
|
||||||
|
|
||||||
Instances.set_consistently_unreachable(sender_url)
|
Instances.set_consistently_unreachable(sender_url)
|
||||||
refute Instances.reachable?(sender_url)
|
refute Instances.reachable?(sender_url)
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign(:valid_signature, true)
|
||||||
|
|> put_req_header("signature", "keyId=\"#{sender.ap_id}/main-key\"")
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/inbox", data)
|
|> post("/inbox", data)
|
||||||
|
|
||||||
|
@ -632,6 +640,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
assert "ok" ==
|
assert "ok" ==
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign(:valid_signature, true)
|
||||||
|
|> put_req_header("signature", "keyId=\"#{followed_relay.ap_id}/main-key\"")
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/inbox", accept)
|
|> post("/inbox", accept)
|
||||||
|> json_response(200)
|
|> json_response(200)
|
||||||
|
@ -698,6 +707,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
|
|
||||||
actor = "https://example.com/users/lain"
|
actor = "https://example.com/users/lain"
|
||||||
|
|
||||||
|
insert(:user,
|
||||||
|
ap_id: actor,
|
||||||
|
featured_address: "https://example.com/users/lain/collections/featured"
|
||||||
|
)
|
||||||
|
|
||||||
Tesla.Mock.mock(fn
|
Tesla.Mock.mock(fn
|
||||||
%{
|
%{
|
||||||
method: :get,
|
method: :get,
|
||||||
|
@ -743,6 +757,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
assert "ok" ==
|
assert "ok" ==
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign(:valid_signature, true)
|
||||||
|
|> put_req_header("signature", "keyId=\"#{actor}/main-key\"")
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/inbox", data)
|
|> post("/inbox", data)
|
||||||
|> json_response(200)
|
|> json_response(200)
|
||||||
|
@ -750,6 +765,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
|
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
|
||||||
assert Activity.get_by_ap_id(data["id"])
|
assert Activity.get_by_ap_id(data["id"])
|
||||||
user = User.get_cached_by_ap_id(data["actor"])
|
user = User.get_cached_by_ap_id(data["actor"])
|
||||||
|
|
||||||
assert user.pinned_objects[data["object"]]
|
assert user.pinned_objects[data["object"]]
|
||||||
|
|
||||||
data = %{
|
data = %{
|
||||||
|
@ -764,6 +780,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
assert "ok" ==
|
assert "ok" ==
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign(:valid_signature, true)
|
||||||
|
|> put_req_header("signature", "keyId=\"#{actor}/main-key\"")
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/inbox", data)
|
|> post("/inbox", data)
|
||||||
|> json_response(200)
|
|> json_response(200)
|
||||||
|
@ -790,6 +807,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
|
|
||||||
actor = "https://example.com/users/lain"
|
actor = "https://example.com/users/lain"
|
||||||
|
|
||||||
|
sender =
|
||||||
|
insert(:user,
|
||||||
|
ap_id: actor,
|
||||||
|
featured_address: "https://example.com/users/lain/collections/featured"
|
||||||
|
)
|
||||||
|
|
||||||
Tesla.Mock.mock(fn
|
Tesla.Mock.mock(fn
|
||||||
%{
|
%{
|
||||||
method: :get,
|
method: :get,
|
||||||
|
@ -844,6 +867,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
assert "ok" ==
|
assert "ok" ==
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign(:valid_signature, true)
|
||||||
|
|> put_req_header("signature", "keyId=\"#{sender.ap_id}/main-key\"")
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/inbox", data)
|
|> post("/inbox", data)
|
||||||
|> json_response(200)
|
|> json_response(200)
|
||||||
|
@ -863,6 +887,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
assert "ok" ==
|
assert "ok" ==
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign(:valid_signature, true)
|
||||||
|
|> put_req_header("signature", "keyId=\"#{actor}/main-key\"")
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/inbox", data)
|
|> post("/inbox", data)
|
||||||
|> json_response(200)
|
|> json_response(200)
|
||||||
|
@ -894,6 +919,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign(:valid_signature, true)
|
||||||
|
|> put_req_header("signature", "keyId=\"#{data["actor"]}/main-key\"")
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/users/#{user.nickname}/inbox", data)
|
|> post("/users/#{user.nickname}/inbox", data)
|
||||||
|
|
||||||
|
@ -915,6 +941,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign(:valid_signature, true)
|
||||||
|
|> put_req_header("signature", "keyId=\"#{data["actor"]}/main-key\"")
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/users/#{user.nickname}/inbox", data)
|
|> post("/users/#{user.nickname}/inbox", data)
|
||||||
|
|
||||||
|
@ -936,6 +963,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign(:valid_signature, true)
|
||||||
|
|> put_req_header("signature", "keyId=\"#{data["actor"]}/main-key\"")
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/users/#{user.nickname}/inbox", data)
|
|> post("/users/#{user.nickname}/inbox", data)
|
||||||
|
|
||||||
|
@ -960,6 +988,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign(:valid_signature, true)
|
||||||
|
|> put_req_header("signature", "keyId=\"#{data["actor"]}/main-key\"")
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/users/#{user.nickname}/inbox", data)
|
|> post("/users/#{user.nickname}/inbox", data)
|
||||||
|
|
||||||
|
@ -987,6 +1016,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign(:valid_signature, true)
|
||||||
|
|> put_req_header("signature", "keyId=\"#{announcer.ap_id}/main-key\"")
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/users/#{user.nickname}/inbox", data)
|
|> post("/users/#{user.nickname}/inbox", data)
|
||||||
|
|
||||||
|
@ -1017,6 +1047,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign(:valid_signature, true)
|
||||||
|
|> put_req_header("signature", "keyId=\"#{actor.ap_id}/main-key\"")
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/users/#{recipient.nickname}/inbox", data)
|
|> post("/users/#{recipient.nickname}/inbox", data)
|
||||||
|
|
||||||
|
@ -1063,6 +1094,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign(:valid_signature, true)
|
||||||
|
|> put_req_header("signature", "keyId=\"#{data["actor"]}/main-key\"")
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/users/#{user.nickname}/inbox", data)
|
|> post("/users/#{user.nickname}/inbox", data)
|
||||||
|
|
||||||
|
@ -1101,6 +1133,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign(:valid_signature, true)
|
||||||
|
|> put_req_header("signature", "keyId=\"#{actor.ap_id}/main-key\"")
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/users/#{recipient.nickname}/inbox", data)
|
|> post("/users/#{recipient.nickname}/inbox", data)
|
||||||
|> json_response(200)
|
|> json_response(200)
|
||||||
|
@ -1193,6 +1226,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign(:valid_signature, true)
|
||||||
|
|> put_req_header("signature", "keyId=\"#{actor.ap_id}/main-key\"")
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/users/#{reported_user.nickname}/inbox", data)
|
|> post("/users/#{reported_user.nickname}/inbox", data)
|
||||||
|> json_response(200)
|
|> json_response(200)
|
||||||
|
@ -1248,6 +1282,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign(:valid_signature, true)
|
||||||
|
|> put_req_header("signature", "keyId=\"#{remote_actor}/main-key\"")
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/users/#{reported_user.nickname}/inbox", data)
|
|> post("/users/#{reported_user.nickname}/inbox", data)
|
||||||
|> json_response(200)
|
|> json_response(200)
|
||||||
|
|
|
@ -9,6 +9,8 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do
|
||||||
import Tesla.Mock
|
import Tesla.Mock
|
||||||
import Plug.Conn
|
import Plug.Conn
|
||||||
|
|
||||||
|
import Pleroma.Tests.Helpers, only: [clear_config: 2]
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
||||||
:ok
|
:ok
|
||||||
|
@ -47,6 +49,26 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do
|
||||||
assert %{valid_signature: false} == conn.assigns
|
assert %{valid_signature: false} == conn.assigns
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it considers a mapped identity to be invalid when the associated instance is blocked" do
|
||||||
|
clear_config([:activitypub, :authorized_fetch_mode], true)
|
||||||
|
|
||||||
|
clear_config([:mrf_simple, :reject], [
|
||||||
|
{"mastodon.example.org", "anime is banned"}
|
||||||
|
])
|
||||||
|
|
||||||
|
on_exit(fn ->
|
||||||
|
Pleroma.Config.put([:activitypub, :authorized_fetch_mode], false)
|
||||||
|
Pleroma.Config.put([:mrf_simple, :reject], [])
|
||||||
|
end)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
|
||||||
|
|> set_signature("http://mastodon.example.org/users/admin")
|
||||||
|
|> MappedSignatureToIdentityPlug.call(%{})
|
||||||
|
|
||||||
|
assert %{valid_signature: false} == conn.assigns
|
||||||
|
end
|
||||||
|
|
||||||
@tag skip: "known breakage; the testsuite presently depends on it"
|
@tag skip: "known breakage; the testsuite presently depends on it"
|
||||||
test "it considers a mapped identity to be invalid when the identity cannot be found" do
|
test "it considers a mapped identity to be invalid when the identity cannot be found" do
|
||||||
conn =
|
conn =
|
||||||
|
|
Loading…
Reference in a new issue