commit
67af50ec71
@ -0,0 +1,31 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug do
|
||||||
|
import Plug.Conn
|
||||||
|
alias Pleroma.Config
|
||||||
|
alias Pleroma.User
|
||||||
|
|
||||||
|
def init(options) do
|
||||||
|
options
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(conn, _) do
|
||||||
|
public? = Config.get!([:instance, :public])
|
||||||
|
|
||||||
|
case {public?, conn} do
|
||||||
|
{true, _} ->
|
||||||
|
conn
|
||||||
|
|
||||||
|
{false, %{assigns: %{user: %User{}}}} ->
|
||||||
|
conn
|
||||||
|
|
||||||
|
{false, _} ->
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/json")
|
||||||
|
|> send_resp(403, Jason.encode!(%{error: "This resource requires authentication."}))
|
||||||
|
|> halt
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,41 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Signature do
|
||||||
|
@behaviour HTTPSignatures.Adapter
|
||||||
|
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
|
alias Pleroma.Web.ActivityPub.Utils
|
||||||
|
alias Pleroma.Web.Salmon
|
||||||
|
alias Pleroma.Web.WebFinger
|
||||||
|
|
||||||
|
def fetch_public_key(conn) do
|
||||||
|
with actor_id <- Utils.get_ap_id(conn.params["actor"]),
|
||||||
|
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
||||||
|
{:ok, public_key}
|
||||||
|
else
|
||||||
|
e ->
|
||||||
|
{:error, e}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def refetch_public_key(conn) do
|
||||||
|
with actor_id <- Utils.get_ap_id(conn.params["actor"]),
|
||||||
|
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
|
||||||
|
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
||||||
|
{:ok, public_key}
|
||||||
|
else
|
||||||
|
e ->
|
||||||
|
{:error, e}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def sign(%User{} = user, headers) do
|
||||||
|
with {:ok, %{info: %{keys: keys}}} <- WebFinger.ensure_keys_present(user),
|
||||||
|
{:ok, private_key, _} <- Salmon.keys_from_pem(keys) do
|
||||||
|
HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,41 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.AdminAPI.ReportView do
|
||||||
|
use Pleroma.Web, :view
|
||||||
|
alias Pleroma.Activity
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.CommonAPI.Utils
|
||||||
|
alias Pleroma.Web.MastodonAPI.AccountView
|
||||||
|
alias Pleroma.Web.MastodonAPI.StatusView
|
||||||
|
|
||||||
|
def render("index.json", %{reports: reports}) do
|
||||||
|
%{
|
||||||
|
reports: render_many(reports, __MODULE__, "show.json", as: :report)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def render("show.json", %{report: report}) do
|
||||||
|
user = User.get_cached_by_ap_id(report.data["actor"])
|
||||||
|
created_at = Utils.to_masto_date(report.data["published"])
|
||||||
|
|
||||||
|
[account_ap_id | status_ap_ids] = report.data["object"]
|
||||||
|
account = User.get_cached_by_ap_id(account_ap_id)
|
||||||
|
|
||||||
|
statuses =
|
||||||
|
Enum.map(status_ap_ids, fn ap_id ->
|
||||||
|
Activity.get_by_ap_id_with_object(ap_id)
|
||||||
|
end)
|
||||||
|
|
||||||
|
%{
|
||||||
|
id: report.id,
|
||||||
|
account: AccountView.render("account.json", %{user: account}),
|
||||||
|
actor: AccountView.render("account.json", %{user: user}),
|
||||||
|
content: report.data["content"],
|
||||||
|
created_at: created_at,
|
||||||
|
statuses: StatusView.render("index.json", %{activities: statuses, as: :activity}),
|
||||||
|
state: report.data["state"]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
@ -1,91 +0,0 @@
|
|||||||
# Pleroma: A lightweight social networking server
|
|
||||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
# https://tools.ietf.org/html/draft-cavage-http-signatures-08
|
|
||||||
defmodule Pleroma.Web.HTTPSignatures do
|
|
||||||
alias Pleroma.User
|
|
||||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
|
||||||
alias Pleroma.Web.ActivityPub.Utils
|
|
||||||
|
|
||||||
require Logger
|
|
||||||
|
|
||||||
def split_signature(sig) do
|
|
||||||
default = %{"headers" => "date"}
|
|
||||||
|
|
||||||
sig =
|
|
||||||
sig
|
|
||||||
|> String.trim()
|
|
||||||
|> String.split(",")
|
|
||||||
|> Enum.reduce(default, fn part, acc ->
|
|
||||||
[key | rest] = String.split(part, "=")
|
|
||||||
value = Enum.join(rest, "=")
|
|
||||||
Map.put(acc, key, String.trim(value, "\""))
|
|
||||||
end)
|
|
||||||
|
|
||||||
Map.put(sig, "headers", String.split(sig["headers"], ~r/\s/))
|
|
||||||
end
|
|
||||||
|
|
||||||
def validate(headers, signature, public_key) do
|
|
||||||
sigstring = build_signing_string(headers, signature["headers"])
|
|
||||||
Logger.debug("Signature: #{signature["signature"]}")
|
|
||||||
Logger.debug("Sigstring: #{sigstring}")
|
|
||||||
{:ok, sig} = Base.decode64(signature["signature"])
|
|
||||||
:public_key.verify(sigstring, :sha256, sig, public_key)
|
|
||||||
end
|
|
||||||
|
|
||||||
def validate_conn(conn) do
|
|
||||||
# TODO: How to get the right key and see if it is actually valid for that request.
|
|
||||||
# For now, fetch the key for the actor.
|
|
||||||
with actor_id <- Utils.get_ap_id(conn.params["actor"]),
|
|
||||||
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
|
||||||
if validate_conn(conn, public_key) do
|
|
||||||
true
|
|
||||||
else
|
|
||||||
Logger.debug("Could not validate, re-fetching user and trying one more time")
|
|
||||||
# Fetch user anew and try one more time
|
|
||||||
with actor_id <- Utils.get_ap_id(conn.params["actor"]),
|
|
||||||
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
|
|
||||||
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
|
||||||
validate_conn(conn, public_key)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
_e ->
|
|
||||||
Logger.debug("Could not public key!")
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def validate_conn(conn, public_key) do
|
|
||||||
headers = Enum.into(conn.req_headers, %{})
|
|
||||||
signature = split_signature(headers["signature"])
|
|
||||||
validate(headers, signature, public_key)
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_signing_string(headers, used_headers) do
|
|
||||||
used_headers
|
|
||||||
|> Enum.map(fn header -> "#{header}: #{headers[header]}" end)
|
|
||||||
|> Enum.join("\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
def sign(user, headers) do
|
|
||||||
with {:ok, %{info: %{keys: keys}}} <- Pleroma.Web.WebFinger.ensure_keys_present(user),
|
|
||||||
{:ok, private_key, _} = Pleroma.Web.Salmon.keys_from_pem(keys) do
|
|
||||||
sigstring = build_signing_string(headers, Map.keys(headers))
|
|
||||||
|
|
||||||
signature =
|
|
||||||
:public_key.sign(sigstring, :sha256, private_key)
|
|
||||||
|> Base.encode64()
|
|
||||||
|
|
||||||
[
|
|
||||||
keyId: user.ap_id <> "#main-key",
|
|
||||||
algorithm: "rsa-sha256",
|
|
||||||
headers: Map.keys(headers) |> Enum.join(" "),
|
|
||||||
signature: signature
|
|
||||||
]
|
|
||||||
|> Enum.map(fn {k, v} -> "#{k}=\"#{v}\"" end)
|
|
||||||
|> Enum.join(",")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -0,0 +1,19 @@
|
|||||||
|
defmodule Pleroma.Repo.Migrations.SetDefaultStateToReports do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
execute """
|
||||||
|
UPDATE activities AS a
|
||||||
|
SET data = jsonb_set(data, '{state}', '"open"', true)
|
||||||
|
WHERE data->>'type' = 'Flag'
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
execute """
|
||||||
|
UPDATE activities AS a
|
||||||
|
SET data = data #- '{state}'
|
||||||
|
WHERE data->>'type' = 'Flag'
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,73 @@
|
|||||||
|
defmodule Pleroma.Repo.Migrations.AddThreadVisibilityFunction do
|
||||||
|
use Ecto.Migration
|
||||||
|
@disable_ddl_transaction true
|
||||||
|
|
||||||
|
def up do
|
||||||
|
statement = """
|
||||||
|
CREATE OR REPLACE FUNCTION thread_visibility(actor varchar, activity_id varchar) RETURNS boolean AS $$
|
||||||
|
DECLARE
|
||||||
|
public varchar := 'https://www.w3.org/ns/activitystreams#Public';
|
||||||
|
child objects%ROWTYPE;
|
||||||
|
activity activities%ROWTYPE;
|
||||||
|
actor_user users%ROWTYPE;
|
||||||
|
author_fa varchar;
|
||||||
|
valid_recipients varchar[];
|
||||||
|
BEGIN
|
||||||
|
--- Fetch our actor.
|
||||||
|
SELECT * INTO actor_user FROM users WHERE users.ap_id = actor;
|
||||||
|
|
||||||
|
--- Fetch our initial activity.
|
||||||
|
SELECT * INTO activity FROM activities WHERE activities.data->>'id' = activity_id;
|
||||||
|
|
||||||
|
LOOP
|
||||||
|
--- Ensure that we have an activity before continuing.
|
||||||
|
--- If we don't, the thread is not satisfiable.
|
||||||
|
IF activity IS NULL THEN
|
||||||
|
RETURN false;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
--- We only care about Create activities.
|
||||||
|
IF activity.data->>'type' != 'Create' THEN
|
||||||
|
RETURN true;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
--- Normalize the child object into child.
|
||||||
|
SELECT * INTO child FROM objects
|
||||||
|
INNER JOIN activities ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id'
|
||||||
|
WHERE COALESCE(activity.data->'object'->>'id', activity.data->>'object') = objects.data->>'id';
|
||||||
|
|
||||||
|
--- Fetch the author's AS2 following collection.
|
||||||
|
SELECT COALESCE(users.follower_address, '') INTO author_fa FROM users WHERE users.ap_id = activity.actor;
|
||||||
|
|
||||||
|
--- Prepare valid recipients array.
|
||||||
|
valid_recipients := ARRAY[actor, public];
|
||||||
|
IF ARRAY[author_fa] && actor_user.following THEN
|
||||||
|
valid_recipients := valid_recipients || author_fa;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
--- Check visibility.
|
||||||
|
IF NOT valid_recipients && activity.recipients THEN
|
||||||
|
--- activity not visible, break out of the loop
|
||||||
|
RETURN false;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
--- If there's a parent, load it and do this all over again.
|
||||||
|
IF (child.data->'inReplyTo' IS NOT NULL) AND (child.data->'inReplyTo' != 'null'::jsonb) THEN
|
||||||
|
SELECT * INTO activity FROM activities
|
||||||
|
INNER JOIN objects ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id'
|
||||||
|
WHERE child.data->>'inReplyTo' = objects.data->>'id';
|
||||||
|
ELSE
|
||||||
|
RETURN true;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql IMMUTABLE;
|
||||||
|
"""
|
||||||
|
|
||||||
|
execute(statement)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
execute("drop function thread_visibility(actor varchar, activity_id varchar)")
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,55 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do
|
||||||
|
use Pleroma.Web.ConnCase, async: true
|
||||||
|
|
||||||
|
alias Pleroma.Config
|
||||||
|
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
|
||||||
|
alias Pleroma.User
|
||||||
|
|
||||||
|
test "it halts if not public and no user is assigned", %{conn: conn} do
|
||||||
|
set_public_to(false)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> EnsurePublicOrAuthenticatedPlug.call(%{})
|
||||||
|
|
||||||
|
assert conn.status == 403
|
||||||
|
assert conn.halted == true
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it continues if public", %{conn: conn} do
|
||||||
|
set_public_to(true)
|
||||||
|
|
||||||
|
ret_conn =
|
||||||
|
conn
|
||||||
|
|> EnsurePublicOrAuthenticatedPlug.call(%{})
|
||||||
|
|
||||||
|
assert ret_conn == conn
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it continues if a user is assigned, even if not public", %{conn: conn} do
|
||||||
|
set_public_to(false)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, %User{})
|
||||||
|
|
||||||
|
ret_conn =
|
||||||
|
conn
|
||||||
|
|> EnsurePublicOrAuthenticatedPlug.call(%{})
|
||||||
|
|
||||||
|
assert ret_conn == conn
|
||||||
|
end
|
||||||
|
|
||||||
|
defp set_public_to(value) do
|
||||||
|
orig = Config.get!([:instance, :public])
|
||||||
|
Config.put([:instance, :public], value)
|
||||||
|
|
||||||
|
on_exit(fn ->
|
||||||
|
Config.put([:instance, :public], orig)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,49 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Mix.Tasks.Pleroma.DatabaseTest do
|
||||||
|
alias Pleroma.Repo
|
||||||
|
alias Pleroma.User
|
||||||
|
use Pleroma.DataCase
|
||||||
|
|
||||||
|
import Pleroma.Factory
|
||||||
|
|
||||||
|
setup_all do
|
||||||
|
Mix.shell(Mix.Shell.Process)
|
||||||
|
|
||||||
|
on_exit(fn ->
|
||||||
|
Mix.shell(Mix.Shell.IO)
|
||||||
|
end)
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "running update_users_following_followers_counts" do
|
||||||
|
test "following and followers count are updated" do
|
||||||
|
[user, user2] = insert_pair(:user)
|
||||||
|
{:ok, %User{following: following, info: info} = user} = User.follow(user, user2)
|
||||||
|
|
||||||
|
assert length(following) == 2
|
||||||
|
assert info.follower_count == 0
|
||||||
|
|
||||||
|
info_cng = Ecto.Changeset.change(info, %{follower_count: 3})
|
||||||
|
|
||||||
|
{:ok, user} =
|
||||||
|
user
|
||||||
|
|> Ecto.Changeset.change(%{following: following ++ following})
|
||||||
|
|> Ecto.Changeset.put_embed(:info, info_cng)
|
||||||
|
|> Repo.update()
|
||||||
|
|
||||||
|
assert length(user.following) == 4
|
||||||
|
assert user.info.follower_count == 3
|
||||||
|
|
||||||
|
assert :ok == Mix.Tasks.Pleroma.Database.run(["update_users_following_followers_counts"])
|
||||||
|
|
||||||
|
user = User.get_by_id(user.id)
|
||||||
|
|
||||||
|
assert length(user.following) == 2
|
||||||
|
assert user.info.follower_count == 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,192 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
||||||
|
use Pleroma.DataCase
|
||||||
|
import Pleroma.Factory
|
||||||
|
alias Pleroma.Config
|
||||||
|
alias Pleroma.Web.ActivityPub.MRF.SimplePolicy
|
||||||
|
|
||||||
|
setup do
|
||||||
|
orig = Config.get!(:mrf_simple)
|
||||||
|
|
||||||
|
Config.put(:mrf_simple,
|
||||||
|
media_removal: [],
|
||||||
|
media_nsfw: [],
|
||||||
|
federated_timeline_removal: [],
|
||||||
|
reject: [],
|
||||||
|
accept: []
|
||||||
|
)
|
||||||
|
|
||||||
|
on_exit(fn ->
|
||||||
|
Config.put(:mrf_simple, orig)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "when :media_removal" do
|
||||||
|
test "is empty" do
|
||||||
|
Config.put([:mrf_simple, :media_removal], [])
|
||||||
|
media_message = build_media_message()
|
||||||
|
local_message = build_local_message()
|
||||||
|
|
||||||
|
assert SimplePolicy.filter(media_message) == {:ok, media_message}
|
||||||
|
assert SimplePolicy.filter(local_message) == {:ok, local_message}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "has a matching host" do
|
||||||
|
Config.put([:mrf_simple, :media_removal], ["remote.instance"])
|
||||||
|
media_message = build_media_message()
|
||||||
|
local_message = build_local_message()
|
||||||
|
|
||||||
|
assert SimplePolicy.filter(media_message) ==
|
||||||
|
{:ok,
|
||||||
|
media_message
|
||||||
|
|> Map.put("object", Map.delete(media_message["object"], "attachment"))}
|
||||||
|
|
||||||
|
assert SimplePolicy.filter(local_message) == {:ok, local_message}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "when :media_nsfw" do
|
||||||
|
test "is empty" do
|
||||||
|
Config.put([:mrf_simple, :media_nsfw], [])
|
||||||
|
media_message = build_media_message()
|
||||||
|
local_message = build_local_message()
|
||||||
|
|
||||||
|
assert SimplePolicy.filter(media_message) == {:ok, media_message}
|
||||||
|
assert SimplePolicy.filter(local_message) == {:ok, local_message}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "has a matching host" do
|
||||||
|
Config.put([:mrf_simple, :media_nsfw], ["remote.instance"])
|
||||||
|
media_message = build_media_message()
|
||||||
|
local_message = build_local_message()
|
||||||
|
|
||||||
|
assert SimplePolicy.filter(media_message) ==
|
||||||
|
{:ok,
|
||||||
|
media_message
|
||||||
|
|> put_in(["object", "tag"], ["foo", "nsfw"])
|
||||||
|
|> put_in(["object", "sensitive"], true)}
|
||||||
|
|
||||||
|
assert SimplePolicy.filter(local_message) == {:ok, local_message}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp build_media_message do
|
||||||
|
%{
|
||||||
|
"actor" => "https://remote.instance/users/bob",
|
||||||
|
"type" => "Create",
|
||||||
|
"object" => %{
|
||||||
|
"attachment" => [%{}],
|
||||||
|
"tag" => ["foo"],
|
||||||
|
"sensitive" => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "when :federated_timeline_removal" do
|
||||||
|
test "is empty" do
|
||||||
|
Config.put([:mrf_simple, :federated_timeline_removal], [])
|
||||||
|
{_, ftl_message} = build_ftl_actor_and_message()
|
||||||
|
local_message = build_local_message()
|
||||||
|
|
||||||
|
assert SimplePolicy.filter(ftl_message) == {:ok, ftl_message}
|
||||||
|
assert SimplePolicy.filter(local_message) == {:ok, local_message}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "has a matching host" do
|
||||||
|
{actor, ftl_message} = build_ftl_actor_and_message()
|
||||||
|
|
||||||
|
ftl_message_actor_host =
|
||||||
|
ftl_message
|
||||||
|
|> Map.fetch!("actor")
|
||||||
|
|> URI.parse()
|
||||||
|
|> Map.fetch!(:host)
|
||||||
|
|
||||||
|
Config.put([:mrf_simple, :federated_timeline_removal], [ftl_message_actor_host])
|
||||||
|
local_message = build_local_message()
|
||||||
|
|
||||||
|
assert {:ok, ftl_message} = SimplePolicy.filter(ftl_message)
|
||||||
|
assert actor.follower_address in ftl_message["to"]
|
||||||
|
refute actor.follower_address in ftl_message["cc"]
|
||||||
|
refute "https://www.w3.org/ns/activitystreams#Public" in ftl_message["to"]
|
||||||
|
assert "https://www.w3.org/ns/activitystreams#Public" in ftl_message["cc"]
|
||||||
|
|
||||||
|
assert SimplePolicy.filter(local_message) == {:ok, local_message}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp build_ftl_actor_and_message do
|
||||||
|
actor = insert(:user)
|
||||||
|
|
||||||
|
{actor,
|
||||||
|
%{
|
||||||
|
"actor" => actor.ap_id,
|
||||||
|
"to" => ["https://www.w3.org/ns/activitystreams#Public", "http://foo.bar/baz"],
|
||||||
|
"cc" => [actor.follower_address, "http://foo.bar/qux"]
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "when :reject" do
|
||||||
|
test "is empty" do
|
||||||
|
Config.put([:mrf_simple, :reject], [])
|
||||||
|
|
||||||
|
remote_message = build_remote_message()
|
||||||
|
|
||||||
|
assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "has a matching host" do
|
||||||
|
Config.put([:mrf_simple, :reject], ["remote.instance"])
|
||||||
|
|
||||||
|
remote_message = build_remote_message()
|
||||||
|
|
||||||
|
assert SimplePolicy.filter(remote_message) == {:reject, nil}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "when :accept" do
|
||||||
|
test "is empty" do
|
||||||
|
Config.put([:mrf_simple, :accept], [])
|
||||||
|
|
||||||
|
local_message = build_local_message()
|
||||||
|
remote_message = build_remote_message()
|
||||||
|
|
||||||
|
assert SimplePolicy.filter(local_message) == {:ok, local_message}
|
||||||
|
assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "is not empty but it doesn't have a matching host" do
|
||||||
|
Config.put([:mrf_simple, :accept], ["non.matching.remote"])
|
||||||
|
|
||||||
|
local_message = build_local_message()
|
||||||
|
remote_message = build_remote_message()
|
||||||
|
|
||||||
|
assert SimplePolicy.filter(local_message) == {:ok, local_message}
|
||||||
|
assert SimplePolicy.filter(remote_message) == {:reject, nil}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "has a matching host" do
|
||||||
|
Config.put([:mrf_simple, :accept], ["remote.instance"])
|
||||||
|
|
||||||
|
local_message = build_local_message()
|
||||||
|
remote_message = build_remote_message()
|
||||||
|
|
||||||
|
assert SimplePolicy.filter(local_message) == {:ok, local_message}
|
||||||
|
assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp build_local_message do
|
||||||
|
%{
|
||||||
|
"actor" => "#{Pleroma.Web.base_url()}/users/alice",
|
||||||
|
"to" => [],
|
||||||
|
"cc" => []
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp build_remote_message do
|
||||||
|
%{"actor" => "https://remote.instance/users/bob"}
|
||||||
|
end
|
||||||
|
end
|
@ -1,194 +0,0 @@
|
|||||||
# Pleroma: A lightweight social networking server
|
|
||||||
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
|
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
# http signatures
|
|
||||||
# Test data from https://tools.ietf.org/html/draft-cavage-http-signatures-08#appendix-C
|
|
||||||
defmodule Pleroma.Web.HTTPSignaturesTest do
|
|
||||||
use Pleroma.DataCase
|
|
||||||
alias Pleroma.Web.HTTPSignatures
|
|
||||||
import Pleroma.Factory
|
|
||||||
import Tesla.Mock
|
|
||||||
|
|
||||||
setup do
|
|
||||||
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
|
|
||||||
@public_key hd(:public_key.pem_decode(File.read!("test/web/http_sigs/pub.key")))
|
|
||||||
|> :public_key.pem_entry_decode()
|
|
||||||
|
|
||||||
@headers %{
|
|
||||||
"(request-target)" => "post /foo?param=value&pet=dog",
|
|
||||||
"host" => "example.com",
|
|
||||||
"date" => "Thu, 05 Jan 2014 21:31:40 GMT",
|
|
||||||
"content-type" => "application/json",
|
|
||||||
"digest" => "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=",
|
|
||||||
"content-length" => "18"
|
|
||||||
}
|
|
||||||
|
|
||||||
@default_signature """
|
|
||||||
keyId="Test",algorithm="rsa-sha256",signature="jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9HpFQlG7N4YcJPteKTu4MWCLyk+gIr0wDgqtLWf9NLpMAMimdfsH7FSWGfbMFSrsVTHNTk0rK3usrfFnti1dxsM4jl0kYJCKTGI/UWkqiaxwNiKqGcdlEDrTcUhhsFsOIo8VhddmZTZ8w="
|
|
||||||
"""
|
|
||||||
|
|
||||||
@basic_signature """
|
|
||||||
keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date",signature="HUxc9BS3P/kPhSmJo+0pQ4IsCo007vkv6bUm4Qehrx+B1Eo4Mq5/6KylET72ZpMUS80XvjlOPjKzxfeTQj4DiKbAzwJAb4HX3qX6obQTa00/qPDXlMepD2JtTw33yNnm/0xV7fQuvILN/ys+378Ysi082+4xBQFwvhNvSoVsGv4="
|
|
||||||
"""
|
|
||||||
|
|
||||||
@all_headers_signature """
|
|
||||||
keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date content-type digest content-length",signature="Ef7MlxLXoBovhil3AlyjtBwAL9g4TN3tibLj7uuNB3CROat/9KaeQ4hW2NiJ+pZ6HQEOx9vYZAyi+7cmIkmJszJCut5kQLAwuX+Ms/mUFvpKlSo9StS2bMXDBNjOh4Auj774GFj4gwjS+3NhFeoqyr/MuN6HsEnkvn6zdgfE2i0="
|
|
||||||
"""
|
|
||||||
|
|
||||||
test "split up a signature" do
|
|
||||||
expected = %{
|
|
||||||
"keyId" => "Test",
|
|
||||||
"algorithm" => "rsa-sha256",
|
|
||||||
"signature" =>
|
|
||||||
"jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9HpFQlG7N4YcJPteKTu4MWCLyk+gIr0wDgqtLWf9NLpMAMimdfsH7FSWGfbMFSrsVTHNTk0rK3usrfFnti1dxsM4jl0kYJCKTGI/UWkqiaxwNiKqGcdlEDrTcUhhsFsOIo8VhddmZTZ8w=",
|
|
||||||
"headers" => ["date"]
|
|
||||||
}
|
|
||||||
|
|
||||||
assert HTTPSignatures.split_signature(@default_signature) == expected
|
|
||||||
end
|
|
||||||
|
|
||||||
test "validates the default case" do
|
|
||||||
signature = HTTPSignatures.split_signature(@default_signature)
|
|
||||||
assert HTTPSignatures.validate(@headers, signature, @public_key)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "validates the basic case" do
|
|
||||||
signature = HTTPSignatures.split_signature(@basic_signature)
|
|
||||||
assert HTTPSignatures.validate(@headers, signature, @public_key)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "validates the all-headers case" do
|
|
||||||
signature = HTTPSignatures.split_signature(@all_headers_signature)
|
|
||||||
assert HTTPSignatures.validate(@headers, signature, @public_key)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "it contructs a signing string" do
|
|
||||||
expected = "date: Thu, 05 Jan 2014 21:31:40 GMT\ncontent-length: 18"
|
|
||||||
assert expected == HTTPSignatures.build_signing_string(@headers, ["date", "content-length"])
|
|
||||||
end
|
|
||||||
|
|
||||||
test "it validates a conn" do
|
|
||||||
public_key_pem =
|
|
||||||
"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnGb42rPZIapY4Hfhxrgn\nxKVJczBkfDviCrrYaYjfGxawSw93dWTUlenCVTymJo8meBlFgIQ70ar4rUbzl6GX\nMYvRdku072d1WpglNHXkjKPkXQgngFDrh2sGKtNB/cEtJcAPRO8OiCgPFqRtMiNM\nc8VdPfPdZuHEIZsJ/aUM38EnqHi9YnVDQik2xxDe3wPghOhqjxUM6eLC9jrjI+7i\naIaEygUdyst9qVg8e2FGQlwAeS2Eh8ygCxn+bBlT5OyV59jSzbYfbhtF2qnWHtZy\nkL7KOOwhIfGs7O9SoR2ZVpTEQ4HthNzainIe/6iCR5HGrao/T8dygweXFYRv+k5A\nPQIDAQAB\n-----END PUBLIC KEY-----\n"
|
|
||||||
|
|
||||||
[public_key] = :public_key.pem_decode(public_key_pem)
|
|
||||||
|
|
||||||
public_key =
|
|
||||||
public_key
|
|
||||||
|> :public_key.pem_entry_decode()
|
|
||||||
|
|
||||||
conn = %{
|
|
||||||
req_headers: [
|
|
||||||
{"host", "localtesting.pleroma.lol"},
|
|
||||||
{"connection", "close"},
|
|
||||||
{"content-length", "2316"},
|
|
||||||
{"user-agent", "http.rb/2.2.2 (Mastodon/2.1.0.rc3; +http://mastodon.example.org/)"},
|
|
||||||
{"date", "Sun, 10 Dec 2017 14:23:49 GMT"},
|
|
||||||
{"digest", "SHA-256=x/bHADMW8qRrq2NdPb5P9fl0lYpKXXpe5h5maCIL0nM="},
|
|
||||||
{"content-type", "application/activity+json"},
|
|
||||||
{"(request-target)", "post /users/demiurge/inbox"},
|
|
||||||
{"signature",
|
|
||||||
"keyId=\"http://mastodon.example.org/users/admin#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"i0FQvr51sj9BoWAKydySUAO1RDxZmNY6g7M62IA7VesbRSdFZZj9/fZapLp6YSuvxUF0h80ZcBEq9GzUDY3Chi9lx6yjpUAS2eKb+Am/hY3aswhnAfYd6FmIdEHzsMrpdKIRqO+rpQ2tR05LwiGEHJPGS0p528NvyVxrxMT5H5yZS5RnxY5X2HmTKEgKYYcvujdv7JWvsfH88xeRS7Jlq5aDZkmXvqoR4wFyfgnwJMPLel8P/BUbn8BcXglH/cunR0LUP7sflTxEz+Rv5qg+9yB8zgBsB4C0233WpcJxjeD6Dkq0EcoJObBR56F8dcb7NQtUDu7x6xxzcgSd7dHm5w==\""}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
assert HTTPSignatures.validate_conn(conn, public_key)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "it validates a conn and fetches the key" do
|
|
||||||
conn = %{
|
|
||||||
params: %{"actor" => "http://mastodon.example.org/users/admin"},
|
|
||||||
req_headers: [
|
|
||||||
{"host", "localtesting.pleroma.lol"},
|
|
||||||
{"x-forwarded-for", "127.0.0.1"},
|
|
||||||
{"connection", "close"},
|
|
||||||
{"content-length", "2307"},
|
|
||||||
{"user-agent", "http.rb/2.2.2 (Mastodon/2.1.0.rc3; +http://mastodon.example.org/)"},
|
|
||||||
{"date", "Sun, 11 Feb 2018 17:12:01 GMT"},
|
|
||||||
{"digest", "SHA-256=UXsAnMtR9c7mi1FOf6HRMtPgGI1yi2e9nqB/j4rZ99I="},
|
|
||||||
{"content-type", "application/activity+json"},
|
|
||||||
{"signature",
|
|
||||||
"keyId=\"http://mastodon.example.org/users/admin#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"qXKqpQXUpC3d9bZi2ioEeAqP8nRMD021CzH1h6/w+LRk4Hj31ARJHDwQM+QwHltwaLDUepshMfz2WHSXAoLmzWtvv7xRwY+mRqe+NGk1GhxVZ/LSrO/Vp7rYfDpfdVtkn36LU7/Bzwxvvaa4ZWYltbFsRBL0oUrqsfmJFswNCQIG01BB52BAhGSCORHKtQyzo1IZHdxl8y80pzp/+FOK2SmHkqWkP9QbaU1qTZzckL01+7M5btMW48xs9zurEqC2sM5gdWMQSZyL6isTV5tmkTZrY8gUFPBJQZgihK44v3qgfWojYaOwM8ATpiv7NG8wKN/IX7clDLRMA8xqKRCOKw==\""},
|
|
||||||
{"(request-target)", "post /users/demiurge/inbox"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
assert HTTPSignatures.validate_conn(conn)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "validate this" do
|
|
||||||
conn = %{
|
|
||||||
params: %{"actor" => "https://niu.moe/users/rye"},
|
|
||||||
req_headers: [
|
|
||||||
{"x-forwarded-for", "149.202.73.191"},
|
|
||||||
{"host", "testing.pleroma.lol"},
|
|
||||||
{"x-cluster-client-ip", "149.202.73.191"},
|
|
||||||
{"connection", "upgrade"},
|
|
||||||
{"content-length", "2396"},
|
|
||||||
{"user-agent", "http.rb/3.0.0 (Mastodon/2.2.0; +https://niu.moe/)"},
|
|
||||||
{"date", "Sun, 18 Feb 2018 20:31:51 GMT"},
|
|
||||||
{"digest", "SHA-256=dzH+vLyhxxALoe9RJdMl4hbEV9bGAZnSfddHQzeidTU="},
|
|
||||||
{"content-type", "application/activity+json"},
|
|
||||||
{"signature",
|
|
||||||
"keyId=\"https://niu.moe/users/rye#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"wtxDg4kIpW7nsnUcVJhBk6SgJeDZOocr8yjsnpDRqE52lR47SH6X7G16r7L1AUJdlnbfx7oqcvomoIJoHB3ghP6kRnZW6MyTMZ2jPoi3g0iC5RDqv6oAmDSO14iw6U+cqZbb3P/odS5LkbThF0UNXcfenVNfsKosIJycFjhNQc54IPCDXYq/7SArEKJp8XwEgzmiC2MdxlkVIUSTQYfjM4EG533cwlZocw1mw72e5mm/owTa80BUZAr0OOuhoWARJV9btMb02ZyAF6SCSoGPTA37wHyfM1Dk88NHf7Z0Aov/Fl65dpRM+XyoxdkpkrhDfH9qAx4iuV2VEWddQDiXHA==\""},
|
|
||||||
{"(request-target)", "post /inbox"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
assert HTTPSignatures.validate_conn(conn)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "validate this too" do
|
|
||||||
conn = %{
|
|
||||||
params: %{"actor" => "https://niu.moe/users/rye"},
|
|
||||||
req_headers: [
|
|
||||||
{"x-forwarded-for", "149.202.73.191"},
|
|
||||||
{"host", "testing.pleroma.lol"},
|
|
||||||
{"x-cluster-client-ip", "149.202.73.191"},
|
|
||||||
{"connection", "upgrade"},
|
|
||||||
{"content-length", "2342"},
|
|
||||||
{"user-agent", "http.rb/3.0.0 (Mastodon/2.2.0; +https://niu.moe/)"},
|
|
||||||
{"date", "Sun, 18 Feb 2018 21:44:46 GMT"},
|
|
||||||
{"digest", "SHA-256=vS8uDOJlyAu78cF3k5EzrvaU9iilHCX3chP37gs5sS8="},
|
|
||||||
{"content-type", "application/activity+json"},
|
|
||||||
{"signature",
|
|
||||||
"keyId=\"https://niu.moe/users/rye#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"IN6fHD8pLiDEf35dOaRHzJKc1wBYh3/Yq0ItaNGxUSbJTd2xMjigZbcsVKzvgYYjglDDN+disGNeD+OBKwMqkXWaWe/lyMc9wHvCH5NMhpn/A7qGLY8yToSt4vh8ytSkZKO6B97yC+Nvy6Fz/yMbvKtFycIvSXCq417cMmY6f/aG+rtMUlTbKO5gXzC7SUgGJCtBPCh1xZzu5/w0pdqdjO46ePNeR6JyJSLLV4hfo3+p2n7SRraxM4ePVCUZqhwS9LPt3Zdhy3ut+IXCZgMVIZggQFM+zXLtcXY5HgFCsFQr5WQDu+YkhWciNWtKFnWfAsnsg5sC330lZ/0Z8Z91yA==\""},
|
|
||||||
{"(request-target)", "post /inbox"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
assert HTTPSignatures.validate_conn(conn)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "it generates a signature" do
|
|
||||||
user = insert(:user)
|
|
||||||
assert HTTPSignatures.sign(user, %{host: "mastodon.example.org"}) =~ "keyId=\""
|
|
||||||
end
|
|
||||||
|
|
||||||
test "this too" do
|
|
||||||
conn = %{
|
|
||||||
params: %{"actor" => "https://mst3k.interlinked.me/users/luciferMysticus"},
|
|
||||||
req_headers: [
|
|
||||||
{"host", "soc.canned-death.us"},
|
|
||||||
{"user-agent", "http.rb/3.0.0 (Mastodon/2.2.0; +https://mst3k.interlinked.me/)"},
|
|
||||||
{"date", "Sun, 11 Mar 2018 12:19:36 GMT"},
|
|
||||||
{"digest", "SHA-256=V7Hl6qDK2m8WzNsjzNYSBISi9VoIXLFlyjF/a5o1SOc="},
|
|
||||||
{"content-type", "application/activity+json"},
|
|
||||||
{"signature",
|
|
||||||
"keyId=\"https://mst3k.interlinked.me/users/luciferMysticus#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"CTYdK5a6lYMxzmqjLOpvRRASoxo2Rqib2VrAvbR5HaTn80kiImj15pCpAyx8IZp53s0Fn/y8MjCTzp+absw8kxx0k2sQAXYs2iy6xhdDUe7iGzz+XLAEqLyZIZfecynaU2nb3Z2XnFDjhGjR1vj/JP7wiXpwp6o1dpDZj+KT2vxHtXuB9585V+sOHLwSB1cGDbAgTy0jx/2az2EGIKK2zkw1KJuAZm0DDMSZalp/30P8dl3qz7DV2EHdDNfaVtrs5BfbDOZ7t1hCcASllzAzgVGFl0BsrkzBfRMeUMRucr111ZG+c0BNOEtJYOHSyZsSSdNknElggCJekONYMYk5ZA==\""},
|
|
||||||
{"x-forwarded-for", "2607:5300:203:2899::31:1337"},
|
|
||||||
{"x-forwarded-host", "soc.canned-death.us"},
|
|
||||||
{"x-forwarded-server", "soc.canned-death.us"},
|
|
||||||
{"connection", "Keep-Alive"},
|
|
||||||
{"content-length", "2006"},
|
|
||||||
{"(request-target)", "post /inbox"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
assert HTTPSignatures.validate_conn(conn)
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,15 +0,0 @@
|
|||||||
-----BEGIN RSA PRIVATE KEY-----
|
|
||||||
MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF
|
|
||||||
NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F
|
|
||||||
UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB
|
|
||||||
AoGBAJR8ZkCUvx5kzv+utdl7T5MnordT1TvoXXJGXK7ZZ+UuvMNUCdN2QPc4sBiA
|
|
||||||
QWvLw1cSKt5DsKZ8UETpYPy8pPYnnDEz2dDYiaew9+xEpubyeW2oH4Zx71wqBtOK
|
|
||||||
kqwrXa/pzdpiucRRjk6vE6YY7EBBs/g7uanVpGibOVAEsqH1AkEA7DkjVH28WDUg
|
|
||||||
f1nqvfn2Kj6CT7nIcE3jGJsZZ7zlZmBmHFDONMLUrXR/Zm3pR5m0tCmBqa5RK95u
|
|
||||||
412jt1dPIwJBANJT3v8pnkth48bQo/fKel6uEYyboRtA5/uHuHkZ6FQF7OUkGogc
|
|
||||||
mSJluOdc5t6hI1VsLn0QZEjQZMEOWr+wKSMCQQCC4kXJEsHAve77oP6HtG/IiEn7
|
|
||||||
kpyUXRNvFsDE0czpJJBvL/aRFUJxuRK91jhjC68sA7NsKMGg5OXb5I5Jj36xAkEA
|
|
||||||
gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW
|
|
||||||
G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI
|
|
||||||
7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA==
|
|
||||||
-----END RSA PRIVATE KEY-----
|
|
@ -1,6 +0,0 @@
|
|||||||
-----BEGIN PUBLIC KEY-----
|
|
||||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCFENGw33yGihy92pDjZQhl0C3
|
|
||||||
6rPJj+CvfSC8+q28hxA161QFNUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6
|
|
||||||
Z4UMR7EOcpfdUE9Hf3m/hs+FUR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJw
|
|
||||||
oYi+1hqp1fIekaxsyQIDAQAB
|
|
||||||
-----END PUBLIC KEY-----
|
|
Loading…
Reference in new issue