Extract keys to their own table, match keyID #816
|
@ -1,46 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Keys do
|
||||
# Native generation of RSA keys is only available since OTP 20+ and in default build conditions
|
||||
# We try at compile time to generate natively an RSA key otherwise we fallback on the old way.
|
||||
try do
|
||||
_ = :public_key.generate_key({:rsa, 2048, 65_537})
|
||||
|
||||
def generate_rsa_pem do
|
||||
key = :public_key.generate_key({:rsa, 2048, 65_537})
|
||||
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
|
||||
pem = :public_key.pem_encode([entry]) |> String.trim_trailing()
|
||||
{:ok, pem}
|
||||
end
|
||||
rescue
|
||||
_ ->
|
||||
def generate_rsa_pem do
|
||||
port = Port.open({:spawn, "openssl genrsa"}, [:binary])
|
||||
|
||||
{:ok, pem} =
|
||||
receive do
|
||||
{^port, {:data, pem}} -> {:ok, pem}
|
||||
end
|
||||
|
||||
Port.close(port)
|
||||
|
||||
if Regex.match?(~r/RSA PRIVATE KEY/, pem) do
|
||||
{:ok, pem}
|
||||
else
|
||||
:error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def keys_from_pem(pem) do
|
||||
with [private_key_code] <- :public_key.pem_decode(pem),
|
||||
private_key <- :public_key.pem_entry_decode(private_key_code),
|
||||
{:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} <- private_key do
|
||||
{:ok, private_key, {:RSAPublicKey, modulus, exponent}}
|
||||
else
|
||||
error -> {:error, error}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,47 +5,25 @@
|
|||
defmodule Pleroma.Signature do
|
||||
@behaviour HTTPSignatures.Adapter
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.Keys
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
|
||||
@known_suffixes ["/publickey", "/main-key", "#key"]
|
||||
alias Pleroma.User.SigningKey
|
||||
require Logger
|
||||
|
||||
def key_id_to_actor_id(key_id) do
|
||||
uri =
|
||||
key_id
|
||||
|> URI.parse()
|
||||
|> Map.put(:fragment, nil)
|
||||
|> Map.put(:query, nil)
|
||||
|> remove_suffix(@known_suffixes)
|
||||
case SigningKey.key_id_to_ap_id(key_id) do
|
||||
nil ->
|
||||
floatingghost marked this conversation as resolved
Outdated
|
||||
# hm, we SHOULD have gotten this in the pipeline before we hit here!
|
||||
Logger.error("Could not figure out who owns the key #{key_id}")
|
||||
{:error, :key_owner_not_found}
|
||||
|
||||
maybe_ap_id = URI.to_string(uri)
|
||||
|
||||
case ObjectValidators.ObjectID.cast(maybe_ap_id) do
|
||||
{:ok, ap_id} ->
|
||||
{:ok, ap_id}
|
||||
|
||||
_ ->
|
||||
case Pleroma.Web.WebFinger.finger(maybe_ap_id) do
|
||||
{:ok, %{"ap_id" => ap_id}} -> {:ok, ap_id}
|
||||
_ -> {:error, maybe_ap_id}
|
||||
end
|
||||
key ->
|
||||
{:ok, key}
|
||||
end
|
||||
end
|
||||
|
||||
defp remove_suffix(uri, [test | rest]) do
|
||||
if not is_nil(uri.path) and String.ends_with?(uri.path, test) do
|
||||
Map.put(uri, :path, String.replace(uri.path, test, ""))
|
||||
else
|
||||
remove_suffix(uri, rest)
|
||||
end
|
||||
end
|
||||
|
||||
defp remove_suffix(uri, []), do: uri
|
||||
|
||||
def fetch_public_key(conn) do
|
||||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
||||
{:ok, %SigningKey{}} <- SigningKey.get_or_fetch_by_key_id(kid),
|
||||
{:ok, actor_id} <- key_id_to_actor_id(kid),
|
||||
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
||||
{:ok, public_key}
|
||||
|
@ -57,8 +35,8 @@ def fetch_public_key(conn) do
|
|||
|
||||
def refetch_public_key(conn) do
|
||||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
||||
{:ok, %SigningKey{}} <- SigningKey.get_or_fetch_by_key_id(kid),
|
||||
{:ok, actor_id} <- key_id_to_actor_id(kid),
|
||||
{: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
|
||||
|
@ -67,9 +45,9 @@ def refetch_public_key(conn) do
|
|||
end
|
||||
end
|
||||
|
||||
def sign(%User{keys: keys} = user, headers) do
|
||||
with {:ok, private_key, _} <- Keys.keys_from_pem(keys) do
|
||||
HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers)
|
||||
def sign(%User{} = user, headers) do
|
||||
with {:ok, private_key} <- SigningKey.private_key(user) do
|
||||
HTTPSignatures.sign(private_key, SigningKey.local_key_id(user.ap_id), headers)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -25,7 +25,6 @@ defmodule Pleroma.User do
|
|||
alias Pleroma.Hashtag
|
||||
alias Pleroma.User.HashtagFollow
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.Keys
|
||||
alias Pleroma.MFA
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
|
@ -43,6 +42,7 @@ defmodule Pleroma.User do
|
|||
alias Pleroma.Web.OAuth
|
||||
alias Pleroma.Web.RelMe
|
||||
alias Pleroma.Workers.BackgroundWorker
|
||||
alias Pleroma.User.SigningKey
|
||||
|
||||
use Pleroma.Web, :verified_routes
|
||||
|
||||
|
@ -101,7 +101,6 @@ defmodule Pleroma.User do
|
|||
field(:password, :string, virtual: true)
|
||||
field(:password_confirmation, :string, virtual: true)
|
||||
field(:keys, :string)
|
||||
field(:public_key, :string)
|
||||
field(:ap_id, :string)
|
||||
field(:avatar, :map, default: %{})
|
||||
field(:local, :boolean, default: true)
|
||||
|
@ -222,6 +221,10 @@ defmodule Pleroma.User do
|
|||
on_replace: :delete
|
||||
)
|
||||
|
||||
# FOR THE FUTURE: We might want to make this a one-to-many relationship
|
||||
# it's entirely possible right now, but we don't have a use case for it
|
||||
has_one(:signing_key, SigningKey, foreign_key: :user_id)
|
||||
|
||||
floatingghost marked this conversation as resolved
Outdated
Oneric
commented
this new this new `has_one` relationship is added, but the old public_key field isn’t removed yet
|
||||
timestamps()
|
||||
end
|
||||
|
||||
|
@ -457,6 +460,7 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
|
|||
|> fix_follower_address()
|
||||
|
||||
struct
|
||||
|> Repo.preload(:signing_key)
|
||||
|> cast(
|
||||
params,
|
||||
[
|
||||
|
@ -466,7 +470,6 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
|
|||
:inbox,
|
||||
:shared_inbox,
|
||||
:nickname,
|
||||
:public_key,
|
||||
:avatar,
|
||||
:ap_enabled,
|
||||
:banner,
|
||||
|
@ -495,6 +498,7 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
|
|||
|> validate_required([:ap_id])
|
||||
|> validate_required([:name], trim: false)
|
||||
|> unique_constraint(:nickname)
|
||||
|> cast_assoc(:signing_key, with: &SigningKey.remote_changeset/2, required: false)
|
||||
|> validate_format(:nickname, @email_regex)
|
||||
|> validate_length(:bio, max: bio_limit)
|
||||
|> validate_length(:name, max: name_limit)
|
||||
|
@ -526,7 +530,6 @@ def update_changeset(struct, params \\ %{}) do
|
|||
:name,
|
||||
:emoji,
|
||||
:avatar,
|
||||
:public_key,
|
||||
:inbox,
|
||||
:shared_inbox,
|
||||
:is_locked,
|
||||
|
@ -570,6 +573,7 @@ def update_changeset(struct, params \\ %{}) do
|
|||
:pleroma_settings_store,
|
||||
&{:ok, Map.merge(struct.pleroma_settings_store, &1)}
|
||||
)
|
||||
|> cast_assoc(:signing_key, with: &SigningKey.remote_changeset/2, requred: false)
|
||||
|> validate_fields(false, struct)
|
||||
end
|
||||
|
||||
|
@ -828,8 +832,10 @@ def put_following_and_follower_and_featured_address(changeset) do
|
|||
end
|
||||
|
||||
defp put_private_key(changeset) do
|
||||
{:ok, pem} = Keys.generate_rsa_pem()
|
||||
put_change(changeset, :keys, pem)
|
||||
ap_id = get_field(changeset, :ap_id)
|
||||
|
||||
changeset
|
||||
|> put_assoc(:signing_key, SigningKey.generate_local_keys(ap_id))
|
||||
end
|
||||
|
||||
defp autofollow_users(user) do
|
||||
|
@ -1146,7 +1152,8 @@ def update_and_set_cache(%{data: %Pleroma.User{} = user} = changeset) do
|
|||
was_superuser_before_update = User.superuser?(user)
|
||||
|
||||
with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
|
||||
set_cache(user)
|
||||
user
|
||||
|> set_cache()
|
||||
end
|
||||
|> maybe_remove_report_notifications(was_superuser_before_update)
|
||||
end
|
||||
|
@ -2051,24 +2058,16 @@ defp create_service_actor(uri, nickname) do
|
|||
|> set_cache()
|
||||
end
|
||||
|
||||
def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
|
||||
key =
|
||||
public_key_pem
|
||||
|> :public_key.pem_decode()
|
||||
|> hd()
|
||||
|> :public_key.pem_entry_decode()
|
||||
|
||||
{:ok, key}
|
||||
end
|
||||
|
||||
def public_key(_), do: {:error, "key not found"}
|
||||
defdelegate public_key(user), to: SigningKey
|
||||
|
||||
def get_public_key_for_ap_id(ap_id) do
|
||||
with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
|
||||
{:ok, public_key} <- public_key(user) do
|
||||
{:ok, public_key} <- SigningKey.public_key(user) do
|
||||
{:ok, public_key}
|
||||
else
|
||||
_ -> :error
|
||||
e ->
|
||||
Logger.error("Could not get public key for #{ap_id}.\n#{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
262
lib/pleroma/user/signing_key.ex
Normal file
|
@ -0,0 +1,262 @@
|
|||
defmodule Pleroma.User.SigningKey do
|
||||
use Ecto.Schema
|
||||
import Ecto.Query
|
||||
import Ecto.Changeset
|
||||
require Pleroma.Constants
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Repo
|
||||
|
||||
require Logger
|
||||
|
||||
@primary_key false
|
||||
schema "signing_keys" do
|
||||
belongs_to(:user, Pleroma.User, type: FlakeId.Ecto.CompatType)
|
||||
field :public_key, :string
|
||||
field :private_key, :string
|
||||
# This is an arbitrary field given by the remote instance
|
||||
field :key_id, :string, primary_key: true
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def load_key(%User{} = user) do
|
||||
user
|
||||
|> Repo.preload(:signing_key)
|
||||
end
|
||||
|
||||
def key_id_of_local_user(%User{local: true} = user) do
|
||||
case Repo.preload(user, :signing_key) do
|
||||
%User{signing_key: %__MODULE__{key_id: key_id}} -> key_id
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec remote_changeset(__MODULE__, map) :: Changeset.t()
|
||||
def remote_changeset(%__MODULE__{} = signing_key, attrs) do
|
||||
signing_key
|
||||
|> cast(attrs, [:public_key, :key_id])
|
||||
|> validate_required([:public_key, :key_id])
|
||||
end
|
||||
|
||||
@spec key_id_to_user_id(String.t()) :: String.t() | nil
|
||||
@doc """
|
||||
Given a key ID, return the user ID associated with that key.
|
||||
Returns nil if the key ID is not found.
|
||||
"""
|
||||
def key_id_to_user_id(key_id) do
|
||||
from(sk in __MODULE__, where: sk.key_id == ^key_id)
|
||||
|> select([sk], sk.user_id)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@spec key_id_to_ap_id(String.t()) :: String.t() | nil
|
||||
@doc """
|
||||
Given a key ID, return the AP ID associated with that key.
|
||||
Returns nil if the key ID is not found.
|
||||
"""
|
||||
def key_id_to_ap_id(key_id) do
|
||||
Logger.debug("Looking up key ID: #{key_id}")
|
||||
|
||||
result =
|
||||
from(sk in __MODULE__, where: sk.key_id == ^key_id)
|
||||
|> join(:inner, [sk], u in User, on: sk.user_id == u.id)
|
||||
|> select([sk, u], %{user: u})
|
||||
|> Repo.one()
|
||||
|
||||
case result do
|
||||
%{user: %User{ap_id: ap_id}} -> ap_id
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec generate_rsa_pem() :: {:ok, binary()}
|
||||
@doc """
|
||||
Generate a new RSA private key and return it as a PEM-encoded string.
|
||||
"""
|
||||
def generate_rsa_pem do
|
||||
key = :public_key.generate_key({:rsa, 2048, 65_537})
|
||||
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
|
||||
pem = :public_key.pem_encode([entry]) |> String.trim_trailing()
|
||||
{:ok, pem}
|
||||
end
|
||||
|
||||
@spec generate_local_keys(String.t()) :: {:ok, Changeset.t()} | {:error, String.t()}
|
||||
@doc """
|
||||
Generate a new RSA key pair and create a changeset for it
|
||||
"""
|
||||
def generate_local_keys(ap_id) do
|
||||
{:ok, private_pem} = generate_rsa_pem()
|
||||
{:ok, local_pem} = private_pem_to_public_pem(private_pem)
|
||||
|
||||
%__MODULE__{}
|
||||
|> change()
|
||||
|> put_change(:public_key, local_pem)
|
||||
|> put_change(:private_key, private_pem)
|
||||
|> put_change(:key_id, local_key_id(ap_id))
|
||||
end
|
||||
|
||||
@spec local_key_id(String.t()) :: String.t()
|
||||
@doc """
|
||||
Given an AP ID, return the key ID for the local user.
|
||||
"""
|
||||
def local_key_id(ap_id) do
|
||||
ap_id <> "#main-key"
|
||||
end
|
||||
|
||||
@spec private_pem_to_public_pem(binary) :: {:ok, binary()} | {:error, String.t()}
|
||||
@doc """
|
||||
Given a private key in PEM format, return the corresponding public key in PEM format.
|
||||
"""
|
||||
def private_pem_to_public_pem(private_pem) do
|
||||
[private_key_code] = :public_key.pem_decode(private_pem)
|
||||
private_key = :public_key.pem_entry_decode(private_key_code)
|
||||
{:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} = private_key
|
||||
public_key = {:RSAPublicKey, modulus, exponent}
|
||||
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
|
||||
{:ok, :public_key.pem_encode([public_key])}
|
||||
end
|
||||
|
||||
@spec public_key(User.t()) :: {:ok, binary()} | {:error, String.t()}
|
||||
@doc """
|
||||
Given a user, return the public key for that user in binary format.
|
||||
"""
|
||||
def public_key(%User{} = user) do
|
||||
case Repo.preload(user, :signing_key) do
|
||||
%User{signing_key: %__MODULE__{public_key: public_key_pem}} ->
|
||||
key =
|
||||
public_key_pem
|
||||
|> :public_key.pem_decode()
|
||||
|> hd()
|
||||
|> :public_key.pem_entry_decode()
|
||||
|
||||
{:ok, key}
|
||||
|
||||
_ ->
|
||||
{:error, "key not found"}
|
||||
end
|
||||
end
|
||||
|
||||
def public_key(_), do: {:error, "key not found"}
|
||||
|
||||
def public_key_pem(%User{} = user) do
|
||||
case Repo.preload(user, :signing_key) do
|
||||
%User{signing_key: %__MODULE__{public_key: public_key_pem}} -> {:ok, public_key_pem}
|
||||
_ -> {:error, "key not found"}
|
||||
end
|
||||
end
|
||||
|
||||
def public_key_pem(_e) do
|
||||
{:error, "key not found"}
|
||||
end
|
||||
|
||||
@spec private_key(User.t()) :: {:ok, binary()} | {:error, String.t()}
|
||||
@doc """
|
||||
Given a user, return the private key for that user in binary format.
|
||||
"""
|
||||
def private_key(%User{} = user) do
|
||||
case Repo.preload(user, :signing_key) do
|
||||
%{signing_key: %__MODULE__{private_key: private_key_pem}} ->
|
||||
key =
|
||||
private_key_pem
|
||||
|> :public_key.pem_decode()
|
||||
|> hd()
|
||||
|> :public_key.pem_entry_decode()
|
||||
|
||||
{:ok, key}
|
||||
|
||||
_ ->
|
||||
{:error, "key not found"}
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_or_fetch_by_key_id(String.t()) :: {:ok, __MODULE__} | {:error, String.t()}
|
||||
@doc """
|
||||
Given a key ID, return the signing key associated with that key.
|
||||
Will either return the key if it exists locally, or fetch it from the remote instance.
|
||||
"""
|
||||
def get_or_fetch_by_key_id(key_id) do
|
||||
case key_id_to_user_id(key_id) do
|
||||
nil ->
|
||||
fetch_remote_key(key_id)
|
||||
|
||||
user_id ->
|
||||
{:ok, Repo.get_by(__MODULE__, user_id: user_id)}
|
||||
end
|
||||
end
|
||||
|
||||
@spec fetch_remote_key(String.t()) :: {:ok, __MODULE__} | {:error, String.t()}
|
||||
@doc """
|
||||
Fetch a remote key by key ID.
|
||||
Will send a request to the remote instance to get the key ID.
|
||||
This request should, at the very least, return a user ID and a public key object.
|
||||
Though bear in mind that some implementations (looking at you, pleroma) may require a signature for this request.
|
||||
This has the potential to create an infinite loop if the remote instance requires a signature to fetch the key...
|
||||
So if we're rejected, we should probably just give up.
|
||||
"""
|
||||
def fetch_remote_key(key_id) do
|
||||
Logger.debug("Fetching remote key: #{key_id}")
|
||||
resp = Pleroma.Object.Fetcher.fetch_and_contain_remote_object_from_id(key_id)
|
||||
|
||||
case resp do
|
||||
{:ok, _body} ->
|
||||
floatingghost
commented
hm. i now i read this back, i don't like the potential for spoofed key IDs. uniq check should probably be user_id/key_id pair? hm. i now i read this back, i don't like the potential for spoofed key IDs. uniq check should probably be user_id/key_id pair?
Oneric
commented
Even with If I understand correctly on failure to verify we’ll refetch the key in case it was recently rotated. Until such a refetch gets triggered though an attacker should be able to impersonate whomever’s key they spoofed. Instead the received data needs to be checked similar to other AP fetches. As mentioned in the other thread, URL comparisons can’t work quite the same way due to key ids not following the usual AP semantics. The checkes decribed in the other thread probably work, but i think it’ll be cleaner to switch to a "refetch once if id differs" approach in A Even with `user_id`+`key_id` key spoofing is possible due to only reading data from the HTTP response body in `handle_signature_Response` and using `Fetcher.get_object` which doesn't verify the authenticity of its data at all; that’s why its comment said `Do NOT use`
E.g. an attacker might trigger lookup of key id `https://evil.com/spoofed#key` but the served response then poses as `https://fluffy.example/users/fox#main-key` belonging to another user.
If I understand correctly on failure to verify we’ll [refetch the key](https://akkoma.dev/AkkomaGang/http_signatures/src/commit/d44c43d66758c6a73eaa4da9cffdbee0c5da44ae/lib/http_signatures/http_signatures.ex#L51) in case it was recently rotated. Until such a refetch gets triggered though an attacker should be able to impersonate whomever’s key they spoofed.
Instead the received data needs to be checked similar to other AP fetches. As mentioned in the other thread, URL comparisons can’t work quite the same way due to key ids not following the usual AP semantics. The checkes decribed in the other thread probably work, but i think it’ll be cleaner to switch to a "refetch once if id differs" approach in `fetch_and_contain`; we’ll end up fetching the user object then (even for non-fragment keys) and can use the top-level id directly as `user_id` and extract its `publicKey`. This allows us to use to share the strict-id logic between normal AP objects and keys
A `user_id`+`key_Id` composite primary key would allow shared keys to work if anyone uses them. But to avoid storing key data over and over again a separate `_user_key` join table would probably be better. Considering such shared keys don’t work with our old key logic either though, i think it’s safe to ignore them for now just like “multiple keys per user”.
Oneric
commented
done in #819 > but i think it’ll be cleaner to switch to a "refetch once if id differs" approach in fetch_and_contain
done in #819
floatingghost
commented
moved to using the contain function, this should be mitigated now i would think? moved to using the contain function, this should be mitigated now i would think?
|
||||
case handle_signature_response(resp) do
|
||||
{:ok, ap_id, public_key_pem} ->
|
||||
Logger.debug("Fetched remote key: #{ap_id}")
|
||||
# fetch the user
|
||||
{:ok, user} = User.get_or_fetch_by_ap_id(ap_id)
|
||||
# store the key
|
||||
key = %__MODULE__{
|
||||
user_id: user.id,
|
||||
public_key: public_key_pem,
|
||||
key_id: key_id
|
||||
}
|
||||
|
||||
Repo.insert(key, on_conflict: :replace_all, conflict_target: :key_id)
|
||||
|
||||
e ->
|
||||
Logger.debug("Failed to fetch remote key: #{inspect(e)}")
|
||||
{:error, "Could not fetch key"}
|
||||
end
|
||||
|
||||
_ ->
|
||||
Logger.debug("Failed to fetch remote key: #{inspect(resp)}")
|
||||
{:error, "Could not fetch key"}
|
||||
end
|
||||
end
|
||||
|
||||
# Take the response from the remote instance and extract the key details
|
||||
# will check if the key ID matches the owner of the key, if not, error
|
||||
defp extract_key_details(%{"id" => ap_id, "publicKey" => public_key}) do
|
||||
if ap_id !== public_key["owner"] do
|
||||
{:error, "Key ID does not match owner"}
|
||||
else
|
||||
%{"publicKeyPem" => public_key_pem} = public_key
|
||||
{:ok, ap_id, public_key_pem}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_signature_response({:ok, body}) do
|
||||
case body do
|
||||
%{
|
||||
"type" => "CryptographicKey",
|
||||
"publicKeyPem" => public_key_pem,
|
||||
"owner" => ap_id
|
||||
} ->
|
||||
{:ok, ap_id, public_key_pem}
|
||||
|
||||
# for when we get a subset of the user object
|
||||
%{
|
||||
"id" => _user_id,
|
||||
"publicKey" => _public_key,
|
||||
"type" => actor_type
|
||||
}
|
||||
when actor_type in Pleroma.Constants.actor_types() ->
|
||||
extract_key_details(body)
|
||||
|
||||
%{"error" => error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_signature_response({:error, e}), do: {:error, e}
|
||||
defp handle_signature_response(other), do: {:error, "Could not fetch key: #{inspect(other)}"}
|
||||
end
|
|
@ -1547,6 +1547,17 @@ defp normalize_attachment(%{} = attachment), do: [attachment]
|
|||
defp normalize_attachment(attachment) when is_list(attachment), do: attachment
|
||||
defp normalize_attachment(_), do: []
|
||||
|
||||
defp maybe_make_public_key_object(data) do
|
||||
if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do
|
||||
%{
|
||||
public_key: data["publicKey"]["publicKeyPem"],
|
||||
key_id: data["publicKey"]["id"]
|
||||
}
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp object_to_user_data(data, additional) do
|
||||
fields =
|
||||
data
|
||||
|
@ -1578,9 +1589,16 @@ defp object_to_user_data(data, additional) do
|
|||
featured_address = data["featured"]
|
||||
{:ok, pinned_objects} = fetch_and_prepare_featured_from_ap_id(featured_address)
|
||||
|
||||
public_key =
|
||||
if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do
|
||||
data["publicKey"]["publicKeyPem"]
|
||||
# first, check that the owner is correct
|
||||
signing_key =
|
||||
if data["id"] !== data["publicKey"]["owner"] do
|
||||
Logger.error(
|
||||
"Owner of the public key is not the same as the actor - not saving the public key."
|
||||
)
|
||||
|
||||
nil
|
||||
else
|
||||
maybe_make_public_key_object(data)
|
||||
end
|
||||
|
||||
shared_inbox =
|
||||
|
@ -1624,7 +1642,7 @@ defp object_to_user_data(data, additional) do
|
|||
bio: data["summary"] || "",
|
||||
actor_type: actor_type,
|
||||
also_known_as: also_known_as,
|
||||
public_key: public_key,
|
||||
signing_key: signing_key,
|
||||
inbox: data["inbox"],
|
||||
shared_inbox: shared_inbox,
|
||||
pinned_objects: pinned_objects,
|
||||
|
|
|
@ -60,7 +60,26 @@ defp relay_active?(conn, _) do
|
|||
end
|
||||
end
|
||||
|
||||
def user(conn, %{"nickname" => nickname}) do
|
||||
@doc """
|
||||
Render the user's AP data
|
||||
WARNING: we cannot actually check if the request has a fragment! so let's play defensively
|
||||
- IF we have a valid signature, serve full user
|
||||
- IF we do not, and authorized_fetch_mode is enabled, serve the key only
|
||||
- OTHERWISE, serve the full actor (since we don't need to worry about the signature)
|
||||
"""
|
||||
def user(%{assigns: %{valid_signature: true}} = conn, params) do
|
||||
render_full_user(conn, params)
|
||||
end
|
||||
|
||||
def user(conn, params) do
|
||||
if Pleroma.Config.get([:activitypub, :authorized_fetch_mode], false) do
|
||||
render_key_only_user(conn, params)
|
||||
else
|
||||
render_full_user(conn, params)
|
||||
end
|
||||
end
|
||||
|
||||
defp render_full_user(conn, %{"nickname" => nickname}) do
|
||||
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
|
||||
conn
|
||||
|> put_resp_content_type("application/activity+json")
|
||||
|
@ -72,6 +91,18 @@ def user(conn, %{"nickname" => nickname}) do
|
|||
end
|
||||
end
|
||||
|
||||
def render_key_only_user(conn, %{"nickname" => nickname}) do
|
||||
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
|
||||
conn
|
||||
|> put_resp_content_type("application/activity+json")
|
||||
|> put_view(UserView)
|
||||
|> render("keys.json", %{user: user})
|
||||
else
|
||||
nil -> {:error, :not_found}
|
||||
%{local: false} -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def object(%{assigns: assigns} = conn, _) do
|
||||
with ap_id <- Endpoint.url() <> conn.request_path,
|
||||
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
|
||||
|
|
|
@ -14,7 +14,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UserValidator do
|
|||
@behaviour Pleroma.Web.ActivityPub.ObjectValidator.Validating
|
||||
|
||||
alias Pleroma.Object.Containment
|
||||
alias Pleroma.Signature
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
|
@ -23,8 +22,7 @@ def validate(object, meta)
|
|||
|
||||
def validate(%{"type" => type, "id" => _id} = data, meta)
|
||||
when type in Pleroma.Constants.actor_types() do
|
||||
with :ok <- validate_pubkey(data),
|
||||
:ok <- validate_inbox(data),
|
||||
with :ok <- validate_inbox(data),
|
||||
:ok <- contain_collection_origin(data) do
|
||||
{:ok, data, meta}
|
||||
else
|
||||
|
@ -35,33 +33,6 @@ def validate(%{"type" => type, "id" => _id} = data, meta)
|
|||
|
||||
def validate(_, _), do: {:error, "Not a user object"}
|
||||
|
||||
defp mabye_validate_owner(nil, _actor), do: :ok
|
||||
defp mabye_validate_owner(actor, actor), do: :ok
|
||||
defp mabye_validate_owner(_owner, _actor), do: :error
|
||||
|
||||
defp validate_pubkey(
|
||||
%{"id" => id, "publicKey" => %{"id" => pk_id, "publicKeyPem" => _key}} = data
|
||||
)
|
||||
when id != nil do
|
||||
with {_, {:ok, kactor}} <- {:key, Signature.key_id_to_actor_id(pk_id)},
|
||||
true <- id == kactor,
|
||||
:ok <- mabye_validate_owner(Map.get(data, "owner"), id) do
|
||||
:ok
|
||||
else
|
||||
{:key, _} ->
|
||||
{:error, "Unable to determine actor id from key id"}
|
||||
|
||||
false ->
|
||||
{:error, "Key id does not relate to user id"}
|
||||
|
||||
_ ->
|
||||
{:error, "Actor does not own its public key"}
|
||||
end
|
||||
end
|
||||
|
||||
# pubkey is optional atm
|
||||
defp validate_pubkey(_data), do: :ok
|
||||
|
||||
defp validate_inbox(%{"id" => id, "inbox" => inbox}) do
|
||||
case Containment.same_origin(id, inbox) do
|
||||
:ok -> :ok
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
defmodule Pleroma.Web.ActivityPub.UserView do
|
||||
use Pleroma.Web, :view
|
||||
|
||||
alias Pleroma.Keys
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
@ -33,9 +32,7 @@ def render("endpoints.json", %{user: %User{local: true} = _user}) do
|
|||
def render("endpoints.json", _), do: %{}
|
||||
|
||||
def render("service.json", %{user: user}) do
|
||||
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
|
||||
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
|
||||
public_key = :public_key.pem_encode([public_key])
|
||||
{:ok, public_key} = User.SigningKey.public_key_pem(user)
|
||||
|
||||
endpoints = render("endpoints.json", %{user: user})
|
||||
|
||||
|
@ -52,7 +49,7 @@ def render("service.json", %{user: user}) do
|
|||
"url" => user.ap_id,
|
||||
"manuallyApprovesFollowers" => false,
|
||||
"publicKey" => %{
|
||||
"id" => "#{user.ap_id}#main-key",
|
||||
"id" => User.SigningKey.local_key_id(user.ap_id),
|
||||
"owner" => user.ap_id,
|
||||
"publicKeyPem" => public_key
|
||||
},
|
||||
|
@ -70,9 +67,12 @@ def render("user.json", %{user: %User{nickname: "internal." <> _} = user}),
|
|||
do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname)
|
||||
|
||||
def render("user.json", %{user: user}) do
|
||||
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
|
||||
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
|
||||
public_key = :public_key.pem_encode([public_key])
|
||||
public_key =
|
||||
case User.SigningKey.public_key_pem(user) do
|
||||
{:ok, public_key} -> public_key
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
user = User.sanitize_html(user)
|
||||
|
||||
endpoints = render("endpoints.json", %{user: user})
|
||||
|
@ -97,7 +97,7 @@ def render("user.json", %{user: user}) do
|
|||
"url" => user.ap_id,
|
||||
"manuallyApprovesFollowers" => user.is_locked,
|
||||
"publicKey" => %{
|
||||
"id" => "#{user.ap_id}#main-key",
|
||||
"id" => User.SigningKey.local_key_id(user.ap_id),
|
||||
"owner" => user.ap_id,
|
||||
"publicKeyPem" => public_key
|
||||
},
|
||||
|
@ -116,6 +116,20 @@ def render("user.json", %{user: user}) do
|
|||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
def render("keys.json", %{user: user}) do
|
||||
{:ok, public_key} = User.SigningKey.public_key_pem(user)
|
||||
|
||||
%{
|
||||
"id" => user.ap_id,
|
||||
"publicKey" => %{
|
||||
"id" => User.SigningKey.key_id_of_local_user(user),
|
||||
"owner" => user.ap_id,
|
||||
"publicKeyPem" => public_key
|
||||
}
|
||||
}
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
def render("following.json", %{user: user, page: page} = opts) do
|
||||
showing_items = (opts[:for] && opts[:for] == user) || !user.hide_follows
|
||||
showing_count = showing_items || !user.hide_follows_count
|
||||
|
|
32
lib/pleroma/web/plugs/ensure_user_public_key_plug.ex
Normal file
|
@ -0,0 +1,32 @@
|
|||
defmodule Pleroma.Web.Plugs.EnsureUserPublicKeyPlug do
|
||||
@moduledoc """
|
||||
This plug will attempt to pull in a user's public key if we do not have it.
|
||||
We _should_ be able to request the URL from the key URL...
|
||||
"""
|
||||
|
||||
alias Pleroma.User
|
||||
|
||||
def init(options), do: options
|
||||
|
||||
def call(conn, _opts) do
|
||||
key_id = key_id_from_conn(conn)
|
||||
|
||||
unless is_nil(key_id) do
|
||||
User.SigningKey.get_or_fetch_by_key_id(key_id)
|
||||
# now we SHOULD have the user that owns the key locally. maybe.
|
||||
# if we don't, we'll error out when we try to validate.
|
||||
end
|
||||
|
||||
conn
|
||||
end
|
||||
|
||||
defp key_id_from_conn(conn) do
|
||||
case HTTPSignatures.signature_for_conn(conn) do
|
||||
%{"keyId" => key_id} when is_binary(key_id) ->
|
||||
key_id
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -139,12 +139,17 @@ defp maybe_require_signature(
|
|||
defp maybe_require_signature(conn), do: conn
|
||||
|
||||
defp signature_host(conn) do
|
||||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
||||
{:ok, actor_id} <- Signature.key_id_to_actor_id(kid) do
|
||||
with {:key_id, %{"keyId" => kid}} <- {:key_id, HTTPSignatures.signature_for_conn(conn)},
|
||||
{:actor_id, {:ok, actor_id}} <- {:actor_id, Signature.key_id_to_actor_id(kid)} do
|
||||
actor_id
|
||||
else
|
||||
e ->
|
||||
{:error, e}
|
||||
{:key_id, e} ->
|
||||
Logger.error("Failed to extract key_id from signature: #{inspect(e)}")
|
||||
nil
|
||||
|
||||
{:actor_id, e} ->
|
||||
Logger.error("Failed to extract actor_id from signature: #{inspect(e)}")
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
|
||||
alias Pleroma.Helpers.AuthHelper
|
||||
alias Pleroma.Signature
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
|
||||
|
@ -33,7 +32,7 @@ def call(%{assigns: %{valid_signature: true}, params: %{"actor" => actor}} = con
|
|||
|> assign(:valid_signature, false)
|
||||
|
||||
# remove me once testsuite uses mapped capabilities instead of what we do now
|
||||
{:user, nil} ->
|
||||
{:user, _} ->
|
||||
Logger.debug("Failed to map identity from signature (lookup failure)")
|
||||
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}")
|
||||
|
||||
|
@ -68,7 +67,8 @@ def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
|
|||
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)
|
||||
conn
|
||||
|> assign(:valid_signature, false)
|
||||
|
||||
_ ->
|
||||
Logger.debug("Failed to map identity from signature (no payload actor mismatch)")
|
||||
|
@ -82,33 +82,34 @@ def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
|
|||
# no signature at all
|
||||
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
|
||||
with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn),
|
||||
{:ok, ap_id} <- Signature.key_id_to_actor_id(key_id) do
|
||||
ap_id
|
||||
else
|
||||
case HTTPSignatures.signature_for_conn(conn) do
|
||||
%{"keyId" => key_id} when is_binary(key_id) ->
|
||||
key_id
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp user_from_key_id(conn) do
|
||||
with key_actor_id when is_binary(key_actor_id) <- key_id_from_conn(conn),
|
||||
{:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(key_actor_id) do
|
||||
with {:key_id, key_id} when is_binary(key_id) <- {:key_id, key_id_from_conn(conn)},
|
||||
{:mapped_ap_id, ap_id} when is_binary(ap_id) <-
|
||||
{:mapped_ap_id, User.SigningKey.key_id_to_ap_id(key_id)},
|
||||
{:user_fetch, {:ok, %User{} = user}} <- {:user_fetch, User.get_or_fetch_by_ap_id(ap_id)} do
|
||||
user
|
||||
else
|
||||
_ ->
|
||||
nil
|
||||
{:key_id, nil} ->
|
||||
Logger.debug("Failed to map identity from signature (no key ID)")
|
||||
{:key_id, nil}
|
||||
|
||||
{:mapped_ap_id, nil} ->
|
||||
Logger.debug("Failed to map identity from signature (could not map key ID to AP ID)")
|
||||
{:mapped_ap_id, nil}
|
||||
|
||||
{:user_fetch, {:error, _}} ->
|
||||
Logger.debug("Failed to map identity from signature (lookup failure)")
|
||||
{:user_fetch, nil}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -144,7 +144,14 @@ defmodule Pleroma.Web.Router do
|
|||
})
|
||||
end
|
||||
|
||||
pipeline :optional_http_signature do
|
||||
plug(Pleroma.Web.Plugs.EnsureUserPublicKeyPlug)
|
||||
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
|
||||
plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
|
||||
end
|
||||
|
||||
pipeline :http_signature do
|
||||
plug(Pleroma.Web.Plugs.EnsureUserPublicKeyPlug)
|
||||
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
|
||||
plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
|
||||
plug(Pleroma.Web.Plugs.EnsureHTTPSignaturePlug)
|
||||
|
@ -745,7 +752,7 @@ defmodule Pleroma.Web.Router do
|
|||
scope "/", Pleroma.Web do
|
||||
# Note: html format is supported only if static FE is enabled
|
||||
# Note: http signature is only considered for json requests (no auth for non-json requests)
|
||||
pipe_through([:accepts_html_xml_json, :http_signature, :static_fe])
|
||||
pipe_through([:accepts_html_xml_json, :optional_http_signature, :static_fe])
|
||||
|
||||
# Note: returns user _profile_ for json requests, redirects to user _feed_ for non-json ones
|
||||
get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed)
|
||||
|
|
4
mix.exs
|
@ -144,7 +144,7 @@ defp deps do
|
|||
{:ex_aws, "~> 2.4"},
|
||||
{:ex_aws_s3, "~> 2.4"},
|
||||
{:sweet_xml, "~> 0.7"},
|
||||
{:earmark, "~> 1.4"},
|
||||
{:earmark, "1.4.46"},
|
||||
{:bbcode_pleroma, "~> 0.2.0"},
|
||||
{:argon2_elixir, "~> 3.1"},
|
||||
{:cors_plug, "~> 3.0"},
|
||||
|
@ -200,7 +200,7 @@ defp deps do
|
|||
|
||||
## dev & test
|
||||
{:ex_doc, "~> 0.30", only: :dev, runtime: false},
|
||||
{:ex_machina, "~> 2.7", only: :test},
|
||||
{:ex_machina, "~> 2.8", only: :test},
|
||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||
{:mock, "~> 0.3.8", only: :test},
|
||||
{:excoveralls, "0.16.1", only: :test},
|
||||
|
|
63
mix.lock
|
@ -8,50 +8,50 @@
|
|||
"cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"},
|
||||
"calendar": {:hex, :calendar, "1.0.0", "f52073a708528482ec33d0a171954ca610fe2bd28f1e871f247dc7f1565fa807", [:mix], [{:tzdata, "~> 0.1.201603 or ~> 0.5.20 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "990e9581920c82912a5ee50e62ff5ef96da6b15949a2ee4734f935fdef0f0a6f"},
|
||||
"captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "90f6ce7672f70f56708792a98d98bd05176c9176", [ref: "90f6ce7672f70f56708792a98d98bd05176c9176"]},
|
||||
"castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"},
|
||||
"castore": {:hex, :castore, "1.0.9", "5cc77474afadf02c7c017823f460a17daa7908e991b0cc917febc90e466a375c", [:mix], [], "hexpm", "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"},
|
||||
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
|
||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||
"comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"},
|
||||
"comeonin": {:hex, :comeonin, "5.5.0", "364d00df52545c44a139bad919d7eacb55abf39e86565878e17cebb787977368", [:mix], [], "hexpm", "6287fc3ba0aad34883cbe3f7949fc1d1e738e5ccdce77165bc99490aa69f47fb"},
|
||||
"concurrent_limiter": {:git, "https://akkoma.dev/AkkomaGang/concurrent-limiter.git", "a9e0b3d64574bdba761f429bb4fba0cf687b3338", [ref: "a9e0b3d64574bdba761f429bb4fba0cf687b3338"]},
|
||||
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
|
||||
"cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"},
|
||||
"cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"},
|
||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
||||
"cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"},
|
||||
"credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"},
|
||||
"credo": {:hex, :credo, "1.7.8", "9722ba1681e973025908d542ec3d95db5f9c549251ba5b028e251ad8c24ab8c5", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cb9e87cc64f152f3ed1c6e325e7b894dea8f5ef2e41123bd864e3cd5ceb44968"},
|
||||
"custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"},
|
||||
"db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
|
||||
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
|
||||
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
|
||||
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
|
||||
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
|
||||
"dialyxir": {:hex, :dialyxir, "1.4.4", "fb3ce8741edeaea59c9ae84d5cec75da00fa89fe401c72d6e047d11a61f65f70", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "cd6111e8017ccd563e65621a4d9a4a1c5cd333df30cebc7face8029cacb4eff6"},
|
||||
"earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"},
|
||||
"eblurhash": {:hex, :eblurhash, "1.2.2", "7da4255aaea984b31bb71155f673257353b0e0554d0d30dcf859547e74602582", [:rebar3], [], "hexpm", "8c20ca00904de023a835a9dcb7b7762fed32264c85a80c3cafa85288e405044c"},
|
||||
"ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"},
|
||||
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
|
||||
"ecto_psql_extras": {:hex, :ecto_psql_extras, "0.8.0", "440719cd74f09b3f01c84455707a2c3972b725c513808e68eb6c5b0ab82bf523", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 0.18.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1 or ~> 4.0.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "f1512812dc196bcb932a96c82e55f69b543dc125e9d39f5e3631a9c4ec65ef12"},
|
||||
"ecto_psql_extras": {:hex, :ecto_psql_extras, "0.8.2", "79350a53246ac5ec27326d208496aebceb77fa82a91744f66a9154560f0759d3", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "> 0.16.0 and < 0.20.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1 or ~> 4.0.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "6149c1c4a5ba6602a76cb09ee7a269eb60dab9694a1dbbb797f032555212de75"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.10.2", "6b98b46534b5c2f8b8b5f03f126e75e2a73c64f3c071149d32987a5378b0fdbd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "68c018debca57cb9235e3889affdaec7a10616a4e3a80c99fa1d01fdafaa9007"},
|
||||
"elasticsearch": {:git, "https://akkoma.dev/AkkomaGang/elasticsearch-elixir.git", "6cd946f75f6ab9042521a009d1d32d29a90113ca", [ref: "main"]},
|
||||
"elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"},
|
||||
"elixir_xml_to_map": {:hex, :elixir_xml_to_map, "3.1.0", "4d6260486a8cce59e4bf3575fe2dd2a24766546ceeef9f93fcec6f7c62a2827a", [:mix], [{:erlsom, "~> 1.4", [hex: :erlsom, repo: "hexpm", optional: false]}], "hexpm", "8fe5f2e75f90bab07ee2161120c2dc038ebcae8135554f5582990f1c8c21f911"},
|
||||
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
|
||||
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
|
||||
"erlsom": {:hex, :erlsom, "1.5.1", "c8fe2babd33ff0846403f6522328b8ab676f896b793634cfe7ef181c05316c03", [:rebar3], [], "hexpm", "7965485494c5844dd127656ac40f141aadfa174839ec1be1074e7edf5b4239eb"},
|
||||
"eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"},
|
||||
"ex_aws": {:hex, :ex_aws, "2.5.4", "86c5bb870a49e0ab6f5aa5dd58cf505f09d2624ebe17530db3c1b61c88a673af", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e82bd0091bb9a5bb190139599f922ff3fc7aebcca4374d65c99c4e23aa6d1625"},
|
||||
"ex_aws_s3": {:hex, :ex_aws_s3, "2.5.3", "422468e5c3e1a4da5298e66c3468b465cfd354b842e512cb1f6fbbe4e2f5bdaf", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "4f09dd372cc386550e484808c5ac5027766c8d0cd8271ccc578b82ee6ef4f3b8"},
|
||||
"ex_aws": {:hex, :ex_aws, "2.5.6", "6f642e0f82eff10a9b470044f084b81a791cf15b393d647ea5f3e65da2794e3d", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.3", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c69eec59e31fdd89d0beeb1d97e16518dd1b23ad95b3d5c9f1dcfec23d97f960"},
|
||||
"ex_aws_s3": {:hex, :ex_aws_s3, "2.5.4", "87aaf4a2f24a48f516d7f5aaced9d128dd5d0f655c4431f9037a11a85c71109c", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "c06e7f68b33f7c0acba1361dbd951c79661a28f85aa2e0582990fccca4425355"},
|
||||
"ex_const": {:hex, :ex_const, "0.3.0", "9d79516679991baf540ef445438eef1455ca91cf1a3c2680d8fb9e5bea2fe4de", [:mix], [], "hexpm", "76546322abb9e40ee4a2f454cf1c8a5b25c3672fa79bed1ea52c31e0d2428ca9"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.34.0", "ab95e0775db3df71d30cf8d78728dd9261c355c81382bcd4cefdc74610bef13e", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "60734fb4c1353f270c3286df4a0d51e65a2c1d9fba66af3940847cc65a8066d7"},
|
||||
"ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"},
|
||||
"ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"},
|
||||
"ex_syslogger": {:hex, :ex_syslogger, "2.0.0", "de6de5c5472a9c4fdafb28fa6610e381ae79ebc17da6490b81d785d68bd124c9", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "a52b2fe71764e9e6ecd149ab66635812f68e39279cbeee27c52c0e35e8b8019e"},
|
||||
"excoveralls": {:hex, :excoveralls, "0.16.1", "0bd42ed05c7d2f4d180331a20113ec537be509da31fed5c8f7047ce59ee5a7c5", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "dae763468e2008cf7075a64cb1249c97cb4bc71e236c5c2b5e5cdf1cfa2bf138"},
|
||||
"expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"},
|
||||
"fast_html": {:hex, :fast_html, "2.3.0", "08c1d8ead840dd3060ba02c761bed9f37f456a1ddfe30bcdcfee8f651cec06a6", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "f18e3c7668f82d3ae0b15f48d48feeb257e28aa5ab1b0dbf781c7312e5da029d"},
|
||||
"fast_sanitize": {:hex, :fast_sanitize, "0.2.3", "67b93dfb34e302bef49fec3aaab74951e0f0602fd9fa99085987af05bd91c7a5", [:mix], [{:fast_html, "~> 2.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "e8ad286d10d0386e15d67d0ee125245ebcfbc7d7290b08712ba9013c8c5e56e2"},
|
||||
"file_ex": {:git, "https://akkoma.dev/AkkomaGang/file_ex.git", "cc7067c7d446c2526e9ecf91d40896b088851569", [ref: "cc7067c7d446c2526e9ecf91d40896b088851569"]},
|
||||
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
|
||||
"file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
|
||||
"finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},
|
||||
"flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"},
|
||||
"floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"},
|
||||
"floki": {:hex, :floki, "0.36.3", "1102f93b16a55bc5383b85ae3ec470f82dee056eaeff9195e8afdf0ef2a43c30", [:mix], [], "hexpm", "fe0158bff509e407735f6d40b3ee0d7deb47f3f3ee7c6c182ad28599f9f6b27a"},
|
||||
"gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"},
|
||||
"gettext": {:hex, :gettext, "0.22.3", "c8273e78db4a0bb6fba7e9f0fd881112f349a3117f7f7c598fa18c66c888e524", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "935f23447713954a6866f1bb28c3a878c4c011e802bcd68a726f5e558e4b64bd"},
|
||||
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
|
||||
|
@ -61,33 +61,34 @@
|
|||
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
|
||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||
"inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"},
|
||||
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
|
||||
"joken": {:hex, :joken, "2.6.1", "2ca3d8d7f83bf7196296a3d9b2ecda421a404634bfc618159981a960020480a1", [:mix], [{:jose, "~> 1.11.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "ab26122c400b3d254ce7d86ed066d6afad27e70416df947cdcb01e13a7382e68"},
|
||||
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||
"joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"},
|
||||
"jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"},
|
||||
"jumper": {:hex, :jumper, "1.0.2", "68cdcd84472a00ac596b4e6459a41b3062d4427cbd4f1e8c8793c5b54f1406a7", [:mix], [], "hexpm", "9b7782409021e01ab3c08270e26f36eb62976a38c1aa64b2eaf6348422f165e1"},
|
||||
"linkify": {:hex, :linkify, "0.5.3", "5f8143d8f61f5ff08d3aeeff47ef6509492b4948d8f08007fbf66e4d2246a7f2", [:mix], [], "hexpm", "3ef35a1377d47c25506e07c1c005ea9d38d700699d92ee92825f024434258177"},
|
||||
"mail": {:hex, :mail, "0.3.1", "cb0a14e4ed8904e4e5a08214e686ccf6f9099346885db17d8c309381f865cc5c", [:mix], [], "hexpm", "1db701e89865c1d5fa296b2b57b1cd587587cca8d8a1a22892b35ef5a8e352a6"},
|
||||
"mail": {:hex, :mail, "0.4.2", "959229676ef11dfdfb1269ce4a611622cb70415a1d6f925d79e547848bafc14d", [:mix], [], "hexpm", "08e5b70c72b8d1605cb88ef2df2c7e41d002210a621503ea1c13f1a7916b6bd3"},
|
||||
"majic": {:git, "https://akkoma.dev/AkkomaGang/majic.git", "80540b36939ec83f48e76c61e5000e0fd67706f0", [ref: "80540b36939ec83f48e76c61e5000e0fd67706f0"]},
|
||||
"makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
|
||||
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
|
||||
"makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"},
|
||||
"makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"},
|
||||
"meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"},
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
"mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "b21ab7754024af096f2d14247574f55f0063295b", [ref: "b21ab7754024af096f2d14247574f55f0063295b"]},
|
||||
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
|
||||
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
|
||||
"mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
|
||||
"mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"},
|
||||
"mock": {:hex, :mock, "0.3.8", "7046a306b71db2488ef54395eeb74df0a7f335a7caca4a3d3875d1fc81c884dd", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "7fa82364c97617d79bb7d15571193fc0c4fe5afd0c932cef09426b3ee6fe2022"},
|
||||
"mogrify": {:hex, :mogrify, "0.9.3", "238c782f00271dace01369ad35ae2e9dd020feee3443b9299ea5ea6bed559841", [:mix], [], "hexpm", "0189b1e1de27455f2b9ae8cf88239cefd23d38de9276eb5add7159aea51731e6"},
|
||||
"mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"},
|
||||
"mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||
"nimble_ownership": {:hex, :nimble_ownership, "1.0.0", "3f87744d42c21b2042a0aa1d48c83c77e6dd9dd357e425a038dd4b49ba8b79a1", [:mix], [], "hexpm", "7c16cc74f4e952464220a73055b557a273e8b1b7ace8489ec9d86e9ad56cb2cc"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
||||
"oban": {:hex, :oban, "2.17.10", "c3e5bd739b5c3fdc38eba1d43ab270a8c6ca4463bb779b7705c69400b0d87678", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4afd027b8e2bc3c399b54318b4f46ee8c40251fb55a285cb4e38b5363f0ee7c4"},
|
||||
"open_api_spex": {:hex, :open_api_spex, "3.19.1", "65ccb5d06e3d664d1eec7c5ea2af2289bd2f37897094a74d7219fb03fc2b5994", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "392895827ce2984a3459c91a484e70708132d8c2c6c5363972b4b91d6bbac3dd"},
|
||||
"oban": {:hex, :oban, "2.17.12", "33fb0cbfb92b910d48dd91a908590fe3698bb85eacec8cd0d9bc6aa13dddd6d6", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7a647d6cd6bb300073db17faabce22d80ae135da3baf3180a064fa7c4fa046e3"},
|
||||
"open_api_spex": {:hex, :open_api_spex, "3.21.2", "6a704f3777761feeb5657340250d6d7332c545755116ca98f33d4b875777e1e5", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "f42ae6ed668b895ebba3e02773cfb4b41050df26f803f2ef634c72a7687dc387"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
|
||||
"phoenix": {:hex, :phoenix, "1.7.12", "1cc589e0eab99f593a8aa38ec45f15d25297dd6187ee801c8de8947090b5a9d3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "d646192fbade9f485b01bc9920c139bfdd19d0f8df3d73fd8eaf2dfbe0d2837c"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.1", "96798325fab2fed5a824ca204e877b81f9afd2e480f581e81f7b4b64a5a477f2", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.17", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "0ae544ff99f3c482b0807c5cec2c8289e810ecacabc04959d82c3337f4703391"},
|
||||
"phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.3", "f686701b0499a07f2e3b122d84d52ff8a31f5def386e03706c916f6feddf69ef", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "909502956916a657a197f94cc1206d9a65247538de8a5e186f7537c895d95764"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"},
|
||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.18.18", "1f38fbd7c363723f19aad1a04b5490ff3a178e37daaf6999594d5f34796c47fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a5810d0472f3189ede6d2a95bda7f31c6113156b91784a3426cb0ab6a6d85214"},
|
||||
|
@ -95,8 +96,8 @@
|
|||
"phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.1", "b74ccaa8046fbc388a62134360ee7d9742d5a8ae74063f34eb050279de7a99e1", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "4000eeba3f9d7d1a6bf56d2bd56733d5cadf41a7f0d8ffe5bb67e7d667e204a2"},
|
||||
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
|
||||
"phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
|
||||
"plug": {:hex, :plug, "1.16.0", "1d07d50cb9bb05097fdf187b31cf087c7297aafc3fed8299aac79c128a707e47", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cbf53aa1f5c4d758a7559c0bd6d59e286c2be0c6a1fac8cc3eee2f638243b93e"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"},
|
||||
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.7.2", "fdadb973799ae691bf9ecad99125b16625b1c6039999da5fe544d99218e662e4", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "245d8a11ee2306094840c000e8816f0cbed69a23fc0ac2bcf8d7835ae019bb2f"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
|
||||
"plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "79fd4fcf34d110605c26560cbae8f23c603ec4158c08298bd4360fdea90bb5cf"},
|
||||
"poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"},
|
||||
|
@ -104,7 +105,7 @@
|
|||
"postgrex": {:hex, :postgrex, "0.17.5", "0483d054938a8dc069b21bdd636bf56c487404c241ce6c319c1f43588246b281", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "50b8b11afbb2c4095a3ba675b4f055c416d0f3d7de6633a595fc131a828a67eb"},
|
||||
"pot": {:hex, :pot, "1.0.2", "13abb849139fdc04ab8154986abbcb63bdee5de6ed2ba7e1713527e33df923dd", [:rebar3], [], "hexpm", "78fe127f5a4f5f919d6ea5a2a671827bd53eb9d37e5b4128c0ad3df99856c2e0"},
|
||||
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
||||
"recon": {:hex, :recon, "2.5.5", "c108a4c406fa301a529151a3bb53158cadc4064ec0c5f99b03ddb8c0e4281bdf", [:mix, :rebar3], [], "hexpm", "632a6f447df7ccc1a4a10bdcfce71514412b16660fe59deca0fcf0aa3c054404"},
|
||||
"recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"},
|
||||
"remote_ip": {:hex, :remote_ip, "1.1.0", "cb308841595d15df3f9073b7c39243a1dd6ca56e5020295cb012c76fbec50f2d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "616ffdf66aaad6a72fc546dabf42eed87e2a99e97b09cbd92b10cc180d02ed74"},
|
||||
"search_parser": {:git, "https://github.com/FloatingGhost/pleroma-contrib-search-parser.git", "08971a81e68686f9ac465cfb6661d51c5e4e1e7f", [ref: "08971a81e68686f9ac465cfb6661d51c5e4e1e7f"]},
|
||||
"sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"},
|
||||
|
@ -114,22 +115,22 @@
|
|||
"swoosh": {:hex, :swoosh, "1.14.4", "94e9dba91f7695a10f49b0172c4a4cb658ef24abef7e8140394521b7f3bbb2d4", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "081c5a590e4ba85cc89baddf7b2beecf6c13f7f84a958f1cd969290815f0f026"},
|
||||
"syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"},
|
||||
"table_rex": {:hex, :table_rex, "4.0.0", "3c613a68ebdc6d4d1e731bc973c233500974ec3993c99fcdabb210407b90959b", [:mix], [], "hexpm", "c35c4d5612ca49ebb0344ea10387da4d2afe278387d4019e4d8111e815df8f55"},
|
||||
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
||||
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
|
||||
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"},
|
||||
"telemetry_metrics_prometheus": {:hex, :telemetry_metrics_prometheus, "1.1.0", "1cc23e932c1ef9aa3b91db257ead31ea58d53229d407e059b29bb962c1505a13", [:mix], [{:plug_cowboy, "~> 2.1", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}], "hexpm", "d43b3659b3244da44fe0275b717701542365d4519b79d9ce895b9719c1ce4d26"},
|
||||
"telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.1.0", "4e15f6d7dbedb3a4e3aed2262b7e1407f166fcb9c30ca3f96635dfbbef99965c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0dd10e7fe8070095df063798f82709b0a1224c31b8baf6278b423898d591a069"},
|
||||
"telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
|
||||
"temple": {:git, "https://akkoma.dev/AkkomaGang/temple.git", "066a699ade472d8fa42a9d730b29a61af9bc8b59", [ref: "066a699ade472d8fa42a9d730b29a61af9bc8b59"]},
|
||||
"tesla": {:hex, :tesla, "1.9.0", "8c22db6a826e56a087eeb8cdef56889731287f53feeb3f361dec5d4c8efb6f14", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "7c240c67e855f7e63e795bf16d6b3f5115a81d1f44b7fe4eadbf656bae0fef8a"},
|
||||
"tesla": {:hex, :tesla, "1.13.0", "24a068a48d107080dd7c943a593997eee265977a38020eb2ab657cca78a12502", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "7b8fc8f6b0640fa0d090af7889d12eb396460e044b6f8688a8e55e30406a2200"},
|
||||
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
|
||||
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"},
|
||||
"tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
|
||||
"tzdata": {:hex, :tzdata, "1.1.2", "45e5f1fcf8729525ec27c65e163be5b3d247ab1702581a94674e008413eef50b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cec7b286e608371602318c414f344941d5eb0375e14cfdab605cca2fe66cba8b"},
|
||||
"ueberauth": {:hex, :ueberauth, "0.10.5", "806adb703df87e55b5615cf365e809f84c20c68aa8c08ff8a416a5a6644c4b02", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3efd1f31d490a125c7ed453b926f7c31d78b97b8a854c755f5c40064bf3ac9e1"},
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
||||
"unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"},
|
||||
"vex": {:hex, :vex, "0.9.2", "fe061acc9e0907d983d46b51bf35d58176f0fe6eb7ba3b33c9336401bf42b6d1", [:mix], [], "hexpm", "76e709a9762e98c6b462dfce92e9b5dfbf712839227f2da8add6dd11549b12cb"},
|
||||
"web_push_encryption": {:hex, :web_push_encryption, "0.3.1", "76d0e7375142dfee67391e7690e89f92578889cbcf2879377900b5620ee4708d", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.11.1", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "4f82b2e57622fb9337559058e8797cb0df7e7c9790793bdc4e40bc895f70e2a2"},
|
||||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"},
|
||||
"websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"},
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
defmodule Pleroma.Repo.Migrations.CreateSigningKeyTable do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:signing_keys, primary_key: false) do
|
||||
add :user_id, references(:users, type: :uuid, on_delete: :delete_all)
|
||||
add :key_id, :text, primary_key: true
|
||||
add :public_key, :text
|
||||
add :private_key, :text
|
||||
Oneric
commented
Might Might `:binary` be more efficient to not have to reparse the text representation each time we actually verify/sign something? On the other hand, for local users, serving the text representation via AP fetches might be more common than actually using the key...
|
||||
timestamps()
|
||||
end
|
||||
end
|
||||
end
|
||||
floatingghost marked this conversation as resolved
Outdated
Oneric
commented
`key_id` already is the primary key; manually creating an index here just duplicates the already existing unique primary key index
|
37
priv/repo/migrations/20240625220752_move_signing_keys.exs
Normal file
|
@ -0,0 +1,37 @@
|
|||
defmodule Pleroma.Repo.Migrations.MoveSigningKeys do
|
||||
use Ecto.Migration
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Repo
|
||||
import Ecto.Query
|
||||
|
||||
def up do
|
||||
# we do not handle remote users here!
|
||||
# because we want to store a key id -> user id mapping, and we don't
|
||||
# currently store key ids for remote users...
|
||||
query =
|
||||
from(u in User)
|
||||
|> where(local: true)
|
||||
|
||||
Repo.stream(query, timeout: :infinity)
|
||||
|> Enum.each(fn
|
||||
%User{id: user_id, keys: private_key, local: true, ap_id: ap_id} ->
|
||||
IO.puts("Migrating user #{user_id}")
|
||||
# we can precompute the public key here...
|
||||
# we do use it on every user view which makes it a bit of a dos attack vector
|
||||
# so we should probably cache it
|
||||
{:ok, public_key} = User.SigningKey.private_pem_to_public_pem(private_key)
|
||||
|
||||
key = %User.SigningKey{
|
||||
user_id: user_id,
|
||||
public_key: public_key,
|
||||
key_id: User.SigningKey.local_key_id(ap_id),
|
||||
private_key: private_key
|
||||
}
|
||||
|
||||
{:ok, _} = Repo.insert(key)
|
||||
end)
|
||||
end
|
||||
|
||||
# no need to rollback
|
||||
def down, do: :ok
|
||||
end
|
|
@ -401,6 +401,7 @@ test "We don't have unexpected tables which may contain objects that are referen
|
|||
["rich_media_card"],
|
||||
["scheduled_activities"],
|
||||
["schema_migrations"],
|
||||
["signing_keys"],
|
||||
["thread_mutes"],
|
||||
["user_follows_hashtag"],
|
||||
["user_frontend_setting_profiles"],
|
||||
|
|
|
@ -64,7 +64,10 @@ test "returns error when zip file is bad", %{pack: pack} do
|
|||
path: Path.absname("test/instance_static/emoji/test_pack/blank.png")
|
||||
}
|
||||
|
||||
assert Pack.add_file(pack, nil, nil, file) == {:error, :bad_eocd}
|
||||
# this varies by erlang OTP
|
||||
possible_error_codes = [:bad_eocd, :einval]
|
||||
{:error, code} = Pack.add_file(pack, nil, nil, file)
|
||||
assert Enum.member?(possible_error_codes, code)
|
||||
end
|
||||
|
||||
test "returns pack when zip file is empty", %{pack: pack} do
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.KeysTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
alias Pleroma.Keys
|
||||
|
||||
test "generates an RSA private key pem" do
|
||||
{:ok, key} = Keys.generate_rsa_pem()
|
||||
|
||||
assert is_binary(key)
|
||||
assert Regex.match?(~r/RSA/, key)
|
||||
end
|
||||
|
||||
test "returns a public and private key from a pem" do
|
||||
pem = File.read!("test/fixtures/private_key.pem")
|
||||
{:ok, private, public} = Keys.keys_from_pem(pem)
|
||||
|
||||
assert elem(private, 0) == :RSAPrivateKey
|
||||
assert elem(public, 0) == :RSAPublicKey
|
||||
end
|
||||
end
|
|
@ -35,25 +35,23 @@ defp make_fake_conn(key_id),
|
|||
do: %Plug.Conn{req_headers: %{"signature" => make_fake_signature(key_id <> "#main-key")}}
|
||||
|
||||
describe "fetch_public_key/1" do
|
||||
test "it returns key" do
|
||||
test "it returns the key" do
|
||||
expected_result = {:ok, @rsa_public_key}
|
||||
|
||||
user = insert(:user, public_key: @public_key)
|
||||
user =
|
||||
insert(:user)
|
||||
|> with_signing_key(public_key: @public_key)
|
||||
|
||||
assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == expected_result
|
||||
end
|
||||
|
||||
test "it returns error when not found user" do
|
||||
assert capture_log(fn ->
|
||||
assert Signature.fetch_public_key(make_fake_conn("https://test-ap-id")) ==
|
||||
{:error, :error}
|
||||
end) =~ "[error] Could not decode user"
|
||||
end
|
||||
|
||||
test "it returns error if public key is nil" do
|
||||
user = insert(:user, public_key: nil)
|
||||
# this actually needs the URL to be valid
|
||||
user = insert(:user)
|
||||
key_id = user.ap_id <> "#main-key"
|
||||
Tesla.Mock.mock(fn %{url: ^key_id} -> {:ok, %{status: 404}} end)
|
||||
|
||||
assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == {:error, :error}
|
||||
assert {:error, _} = Signature.fetch_public_key(make_fake_conn(user.ap_id))
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -63,12 +61,6 @@ test "it returns key" do
|
|||
|
||||
assert Signature.refetch_public_key(make_fake_conn(ap_id)) == {:ok, @rsa_public_key}
|
||||
end
|
||||
|
||||
test "it returns error when not found user" do
|
||||
assert capture_log(fn ->
|
||||
{:error, _} = Signature.refetch_public_key(make_fake_conn("https://test-ap_id"))
|
||||
end) =~ "[error] Could not decode user"
|
||||
end
|
||||
end
|
||||
|
||||
defp split_signature(sig) do
|
||||
|
@ -104,9 +96,9 @@ defp assert_part_equal(part_a, part_b) do
|
|||
test "it returns signature headers" do
|
||||
user =
|
||||
insert(:user, %{
|
||||
ap_id: "https://mastodon.social/users/lambadalambda",
|
||||
keys: @private_key
|
||||
ap_id: "https://mastodon.social/users/lambadalambda"
|
||||
})
|
||||
|> with_signing_key(private_key: @private_key)
|
||||
|
||||
headers = %{
|
||||
host: "test.test",
|
||||
|
@ -121,50 +113,15 @@ test "it returns signature headers" do
|
|||
"keyId=\"https://mastodon.social/users/lambadalambda#main-key\",algorithm=\"rsa-sha256\",headers=\"content-length host\",signature=\"sibUOoqsFfTDerquAkyprxzDjmJm6erYc42W5w1IyyxusWngSinq5ILTjaBxFvfarvc7ci1xAi+5gkBwtshRMWm7S+Uqix24Yg5EYafXRun9P25XVnYBEIH4XQ+wlnnzNIXQkU3PU9e6D8aajDZVp3hPJNeYt1gIPOA81bROI8/glzb1SAwQVGRbqUHHHKcwR8keiR/W2h7BwG3pVRy4JgnIZRSW7fQogKedDg02gzRXwUDFDk0pr2p3q6bUWHUXNV8cZIzlMK+v9NlyFbVYBTHctAR26GIAN6Hz0eV0mAQAePHDY1mXppbA8Gpp6hqaMuYfwifcXmcc+QFm4e+n3A==\""
|
||||
)
|
||||
end
|
||||
|
||||
test "it returns error" do
|
||||
user = insert(:user, %{ap_id: "https://mastodon.social/users/lambadalambda", keys: ""})
|
||||
|
||||
assert Signature.sign(
|
||||
user,
|
||||
%{host: "test.test", "content-length": "100"}
|
||||
) == {:error, []}
|
||||
end
|
||||
end
|
||||
|
||||
describe "key_id_to_actor_id/1" do
|
||||
test "it properly deduces the actor id for misskey" do
|
||||
assert Signature.key_id_to_actor_id("https://example.com/users/1234/publickey") ==
|
||||
{:ok, "https://example.com/users/1234"}
|
||||
end
|
||||
test "it reverses the key id to actor id" do
|
||||
user =
|
||||
insert(:user)
|
||||
|> with_signing_key()
|
||||
|
||||
test "it properly deduces the actor id for mastodon and pleroma" do
|
||||
assert Signature.key_id_to_actor_id("https://example.com/users/1234#main-key") ==
|
||||
{:ok, "https://example.com/users/1234"}
|
||||
end
|
||||
|
||||
test "it deduces the actor id for gotoSocial" do
|
||||
assert Signature.key_id_to_actor_id("https://example.com/users/1234/main-key") ==
|
||||
{:ok, "https://example.com/users/1234"}
|
||||
end
|
||||
|
||||
test "it deduces the actor ID for streams" do
|
||||
assert Signature.key_id_to_actor_id("https://example.com/users/1234?operation=getkey") ==
|
||||
{:ok, "https://example.com/users/1234"}
|
||||
end
|
||||
|
||||
test "it deduces the actor ID for bridgy" do
|
||||
assert Signature.key_id_to_actor_id("https://example.com/1234#key") ==
|
||||
{:ok, "https://example.com/1234"}
|
||||
end
|
||||
|
||||
test "it calls webfinger for 'acct:' accounts" do
|
||||
with_mock(Pleroma.Web.WebFinger,
|
||||
finger: fn _ -> {:ok, %{"ap_id" => "https://gensokyo.2hu/users/raymoo"}} end
|
||||
) do
|
||||
assert Signature.key_id_to_actor_id("acct:raymoo@gensokyo.2hu") ==
|
||||
{:ok, "https://gensokyo.2hu/users/raymoo"}
|
||||
end
|
||||
assert Signature.key_id_to_actor_id(user.signing_key.key_id) == {:ok, user.ap_id}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -259,7 +259,7 @@ test "works with URIs" do
|
|||
|> Map.put(:multi_factor_authentication_settings, nil)
|
||||
|> Map.put(:notification_settings, nil)
|
||||
|
||||
assert user == expected
|
||||
assert_user_match(user, expected)
|
||||
end
|
||||
|
||||
test "excludes a blocked users from search result" do
|
||||
|
|
|
@ -639,11 +639,12 @@ test "it sets the password_hash, ap_id, private key and followers collection add
|
|||
changeset = User.register_changeset(%User{}, @full_user_data)
|
||||
|
||||
assert changeset.valid?
|
||||
|
||||
assert is_binary(changeset.changes[:password_hash])
|
||||
assert is_binary(changeset.changes[:keys])
|
||||
assert changeset.changes[:ap_id] == User.ap_id(%User{nickname: @full_user_data.nickname})
|
||||
assert is_binary(changeset.changes[:keys])
|
||||
assert changeset.changes[:signing_key]
|
||||
assert changeset.changes[:signing_key].valid?
|
||||
assert is_binary(changeset.changes[:signing_key].changes.private_key)
|
||||
assert is_binary(changeset.changes[:signing_key].changes.public_key)
|
||||
assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers"
|
||||
end
|
||||
|
||||
|
@ -1665,7 +1666,6 @@ test "delete/1 purges a user when they wouldn't be fully deleted" do
|
|||
name: "qqqqqqq",
|
||||
password_hash: "pdfk2$1b3n159001",
|
||||
keys: "RSA begin buplic key",
|
||||
public_key: "--PRIVATE KEYE--",
|
||||
avatar: %{"a" => "b"},
|
||||
tags: ["qqqqq"],
|
||||
banner: %{"a" => "b"},
|
||||
|
@ -1704,8 +1704,6 @@ test "delete/1 purges a user when they wouldn't be fully deleted" do
|
|||
email: nil,
|
||||
name: nil,
|
||||
password_hash: nil,
|
||||
keys: "RSA begin buplic key",
|
||||
public_key: "--PRIVATE KEYE--",
|
||||
avatar: %{},
|
||||
tags: [],
|
||||
last_refreshed_at: nil,
|
||||
|
|
|
@ -584,6 +584,7 @@ test "it inserts an incoming activity into the database" <>
|
|||
local: false,
|
||||
last_refreshed_at: nil
|
||||
)
|
||||
|> with_signing_key()
|
||||
|
||||
data =
|
||||
File.read!("test/fixtures/mastodon-post-activity.json")
|
||||
|
@ -594,7 +595,7 @@ test "it inserts an incoming activity into the database" <>
|
|||
conn =
|
||||
conn
|
||||
|> assign(:valid_signature, true)
|
||||
|> put_req_header("signature", "keyId=\"#{user.ap_id}/main-key\"")
|
||||
|> put_req_header("signature", "keyId=\"#{user.signing_key.key_id}\"")
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> post("/inbox", data)
|
||||
|
||||
|
@ -608,7 +609,10 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn} do
|
|||
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
|
||||
|
||||
sender_url = data["actor"]
|
||||
sender = insert(:user, ap_id: data["actor"])
|
||||
|
||||
sender =
|
||||
insert(:user, ap_id: data["actor"])
|
||||
|> with_signing_key()
|
||||
|
||||
Instances.set_consistently_unreachable(sender_url)
|
||||
refute Instances.reachable?(sender_url)
|
||||
|
@ -616,7 +620,7 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn} do
|
|||
conn =
|
||||
conn
|
||||
|> assign(:valid_signature, true)
|
||||
|> put_req_header("signature", "keyId=\"#{sender.ap_id}/main-key\"")
|
||||
|> put_req_header("signature", "keyId=\"#{sender.signing_key.key_id}\"")
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> post("/inbox", data)
|
||||
|
||||
|
@ -641,7 +645,7 @@ test "accept follow activity", %{conn: conn} do
|
|||
assert "ok" ==
|
||||
conn
|
||||
|> assign(:valid_signature, true)
|
||||
|> put_req_header("signature", "keyId=\"#{followed_relay.ap_id}/main-key\"")
|
||||
|> put_req_header("signature", "keyId=\"#{followed_relay.ap_id}#main-key\"")
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> post("/inbox", accept)
|
||||
|> json_response(200)
|
||||
|
@ -678,6 +682,7 @@ test "accepts Add/Remove activities", %{conn: conn} do
|
|||
|> String.replace("{{nickname}}", "lain")
|
||||
|
||||
actor = "https://example.com/users/lain"
|
||||
key_id = "#{actor}/main-key"
|
||||
|
||||
insert(:user,
|
||||
ap_id: actor,
|
||||
|
@ -705,6 +710,16 @@ test "accepts Add/Remove activities", %{conn: conn} do
|
|||
headers: [{"content-type", "application/activity+json"}]
|
||||
}
|
||||
|
||||
%{
|
||||
method: :get,
|
||||
url: ^key_id
|
||||
} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body: user,
|
||||
headers: [{"content-type", "application/activity+json"}]
|
||||
}
|
||||
|
||||
%{method: :get, url: "https://example.com/users/lain/collections/featured"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
|
@ -778,12 +793,14 @@ test "mastodon pin/unpin", %{conn: conn} do
|
|||
|> String.replace("{{nickname}}", "lain")
|
||||
|
||||
actor = "https://example.com/users/lain"
|
||||
key_id = "#{actor}/main-key"
|
||||
|
||||
sender =
|
||||
insert(:user,
|
||||
ap_id: actor,
|
||||
featured_address: "https://example.com/users/lain/collections/featured"
|
||||
)
|
||||
|> with_signing_key()
|
||||
|
||||
Tesla.Mock.mock(fn
|
||||
%{
|
||||
|
@ -806,6 +823,16 @@ test "mastodon pin/unpin", %{conn: conn} do
|
|||
headers: [{"content-type", "application/activity+json"}]
|
||||
}
|
||||
|
||||
%{
|
||||
method: :get,
|
||||
url: ^key_id
|
||||
} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body: user,
|
||||
headers: [{"content-type", "application/activity+json"}]
|
||||
}
|
||||
|
||||
%{method: :get, url: "https://example.com/users/lain/collections/featured"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
|
@ -839,7 +866,7 @@ test "mastodon pin/unpin", %{conn: conn} do
|
|||
assert "ok" ==
|
||||
conn
|
||||
|> assign(:valid_signature, true)
|
||||
|> put_req_header("signature", "keyId=\"#{sender.ap_id}/main-key\"")
|
||||
|> put_req_header("signature", "keyId=\"#{sender.signing_key.key_id}\"")
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> post("/inbox", data)
|
||||
|> json_response(200)
|
||||
|
@ -901,7 +928,9 @@ test "it inserts an incoming activity into the database", %{conn: conn, data: da
|
|||
end
|
||||
|
||||
test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do
|
||||
user = insert(:user)
|
||||
user =
|
||||
insert(:user)
|
||||
|> with_signing_key()
|
||||
|
||||
data =
|
||||
data
|
||||
|
@ -946,7 +975,9 @@ test "it accepts messages with cc as string instead of array", %{conn: conn, dat
|
|||
end
|
||||
|
||||
test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do
|
||||
user = insert(:user)
|
||||
user =
|
||||
insert(:user)
|
||||
|> with_signing_key()
|
||||
|
||||
data =
|
||||
data
|
||||
|
@ -973,7 +1004,10 @@ test "it accepts announces with to as string instead of array", %{conn: conn} do
|
|||
user = insert(:user)
|
||||
|
||||
{:ok, post} = CommonAPI.post(user, %{status: "hey"})
|
||||
announcer = insert(:user, local: false)
|
||||
|
||||
announcer =
|
||||
insert(:user, local: false)
|
||||
|> with_signing_key()
|
||||
|
||||
data = %{
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
|
@ -988,7 +1022,7 @@ test "it accepts announces with to as string instead of array", %{conn: conn} do
|
|||
conn =
|
||||
conn
|
||||
|> assign(:valid_signature, true)
|
||||
|> put_req_header("signature", "keyId=\"#{announcer.ap_id}/main-key\"")
|
||||
|> put_req_header("signature", "keyId=\"#{announcer.signing_key.key_id}\"")
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> post("/users/#{user.nickname}/inbox", data)
|
||||
|
||||
|
@ -1003,7 +1037,10 @@ test "it accepts messages from actors that are followed by the user", %{
|
|||
data: data
|
||||
} do
|
||||
recipient = insert(:user)
|
||||
actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"})
|
||||
|
||||
actor =
|
||||
insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"})
|
||||
|> with_signing_key()
|
||||
|
||||
{:ok, recipient, actor} = User.follow(recipient, actor)
|
||||
|
||||
|
@ -1019,7 +1056,7 @@ test "it accepts messages from actors that are followed by the user", %{
|
|||
conn =
|
||||
conn
|
||||
|> assign(:valid_signature, true)
|
||||
|> put_req_header("signature", "keyId=\"#{actor.ap_id}/main-key\"")
|
||||
|> put_req_header("signature", "keyId=\"#{actor.signing_key.key_id}\"")
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> post("/users/#{recipient.nickname}/inbox", data)
|
||||
|
||||
|
@ -1056,7 +1093,10 @@ test "it returns a note activity in a collection", %{conn: conn} do
|
|||
end
|
||||
|
||||
test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do
|
||||
user = insert(:user)
|
||||
user =
|
||||
insert(:user, ap_id: data["actor"])
|
||||
|> with_signing_key()
|
||||
|
||||
data = Map.put(data, "bcc", [user.ap_id])
|
||||
|
||||
sender_host = URI.parse(data["actor"]).host
|
||||
|
@ -1066,7 +1106,7 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn, da
|
|||
conn =
|
||||
conn
|
||||
|> assign(:valid_signature, true)
|
||||
|> put_req_header("signature", "keyId=\"#{data["actor"]}/main-key\"")
|
||||
|> put_req_header("signature", "keyId=\"#{user.signing_key.key_id}\"")
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> post("/users/#{user.nickname}/inbox", data)
|
||||
|
||||
|
@ -1077,6 +1117,7 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn, da
|
|||
@tag capture_log: true
|
||||
test "it removes all follower collections but actor's", %{conn: conn} do
|
||||
[actor, recipient] = insert_pair(:user)
|
||||
actor = with_signing_key(actor)
|
||||
|
||||
to = [
|
||||
recipient.ap_id,
|
||||
|
@ -1105,7 +1146,7 @@ test "it removes all follower collections but actor's", %{conn: conn} do
|
|||
|
||||
conn
|
||||
|> assign(:valid_signature, true)
|
||||
|> put_req_header("signature", "keyId=\"#{actor.ap_id}/main-key\"")
|
||||
|> put_req_header("signature", "keyId=\"#{actor.signing_key.key_id}\"")
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> post("/users/#{recipient.nickname}/inbox", data)
|
||||
|> json_response(200)
|
||||
|
@ -1141,7 +1182,11 @@ test "it requires authentication", %{conn: conn} do
|
|||
@tag capture_log: true
|
||||
test "forwarded report", %{conn: conn} do
|
||||
admin = insert(:user, is_admin: true)
|
||||
actor = insert(:user, local: false)
|
||||
|
||||
actor =
|
||||
insert(:user, local: false)
|
||||
|> with_signing_key()
|
||||
|
||||
remote_domain = URI.parse(actor.ap_id).host
|
||||
reported_user = insert(:user)
|
||||
|
||||
|
@ -1198,7 +1243,7 @@ test "forwarded report", %{conn: conn} do
|
|||
|
||||
conn
|
||||
|> assign(:valid_signature, true)
|
||||
|> put_req_header("signature", "keyId=\"#{actor.ap_id}/main-key\"")
|
||||
|> put_req_header("signature", "keyId=\"#{actor.signing_key.key_id}\"")
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> post("/users/#{reported_user.nickname}/inbox", data)
|
||||
|> json_response(200)
|
||||
|
@ -1232,12 +1277,22 @@ test "forwarded report from mastodon", %{conn: conn} do
|
|||
|> File.read!()
|
||||
|> String.replace("{{DOMAIN}}", remote_domain)
|
||||
|
||||
Tesla.Mock.mock(fn %{url: ^remote_actor} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body: mock_json_body,
|
||||
headers: [{"content-type", "application/activity+json"}]
|
||||
}
|
||||
key_url = "#{remote_actor}#main-key"
|
||||
|
||||
Tesla.Mock.mock(fn
|
||||
%{url: ^remote_actor} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body: mock_json_body,
|
||||
headers: [{"content-type", "application/activity+json"}]
|
||||
}
|
||||
|
||||
%{url: ^key_url} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body: mock_json_body,
|
||||
headers: [{"content-type", "application/activity+json"}]
|
||||
}
|
||||
end)
|
||||
|
||||
data = %{
|
||||
|
@ -1254,7 +1309,7 @@ test "forwarded report from mastodon", %{conn: conn} do
|
|||
|
||||
conn
|
||||
|> assign(:valid_signature, true)
|
||||
|> put_req_header("signature", "keyId=\"#{remote_actor}/main-key\"")
|
||||
|> put_req_header("signature", "keyId=\"#{remote_actor}#main-key\"")
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> post("/users/#{reported_user.nickname}/inbox", data)
|
||||
|> json_response(200)
|
||||
|
|
|
@ -140,7 +140,9 @@ test "publish to url with with different ports" do
|
|||
{:ok, %Tesla.Env{status: 200, body: "port 80"}}
|
||||
end)
|
||||
|
||||
actor = insert(:user)
|
||||
actor =
|
||||
insert(:user)
|
||||
|> with_signing_key()
|
||||
|
||||
assert {:ok, %{body: "port 42"}} =
|
||||
Publisher.publish_one(%{
|
||||
|
@ -165,7 +167,10 @@ test "publish to url with with different ports" do
|
|||
Instances,
|
||||
[:passthrough],
|
||||
[] do
|
||||
actor = insert(:user)
|
||||
actor =
|
||||
insert(:user)
|
||||
|> with_signing_key()
|
||||
|
||||
inbox = "http://200.site/users/nick1/inbox"
|
||||
|
||||
assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
|
||||
|
@ -176,7 +181,10 @@ test "publish to url with with different ports" do
|
|||
Instances,
|
||||
[:passthrough],
|
||||
[] do
|
||||
actor = insert(:user)
|
||||
actor =
|
||||
insert(:user)
|
||||
|> with_signing_key()
|
||||
|
||||
inbox = "http://200.site/users/nick1/inbox"
|
||||
|
||||
assert {:ok, _} =
|
||||
|
@ -195,7 +203,10 @@ test "publish to url with with different ports" do
|
|||
Instances,
|
||||
[:passthrough],
|
||||
[] do
|
||||
actor = insert(:user)
|
||||
actor =
|
||||
insert(:user)
|
||||
|> with_signing_key()
|
||||
|
||||
inbox = "http://200.site/users/nick1/inbox"
|
||||
|
||||
assert {:ok, _} =
|
||||
|
@ -214,7 +225,10 @@ test "publish to url with with different ports" do
|
|||
Instances,
|
||||
[:passthrough],
|
||||
[] do
|
||||
actor = insert(:user)
|
||||
actor =
|
||||
insert(:user)
|
||||
|> with_signing_key()
|
||||
|
||||
inbox = "http://404.site/users/nick1/inbox"
|
||||
|
||||
assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
|
||||
|
@ -226,7 +240,10 @@ test "publish to url with with different ports" do
|
|||
Instances,
|
||||
[:passthrough],
|
||||
[] do
|
||||
actor = insert(:user)
|
||||
actor =
|
||||
insert(:user)
|
||||
|> with_signing_key()
|
||||
|
||||
inbox = "http://connrefused.site/users/nick1/inbox"
|
||||
|
||||
assert capture_log(fn ->
|
||||
|
@ -241,7 +258,10 @@ test "publish to url with with different ports" do
|
|||
Instances,
|
||||
[:passthrough],
|
||||
[] do
|
||||
actor = insert(:user)
|
||||
actor =
|
||||
insert(:user)
|
||||
|> with_signing_key()
|
||||
|
||||
inbox = "http://200.site/users/nick1/inbox"
|
||||
|
||||
assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
|
||||
|
@ -253,7 +273,10 @@ test "publish to url with with different ports" do
|
|||
Instances,
|
||||
[:passthrough],
|
||||
[] do
|
||||
actor = insert(:user)
|
||||
actor =
|
||||
insert(:user)
|
||||
|> with_signing_key()
|
||||
|
||||
inbox = "http://connrefused.site/users/nick1/inbox"
|
||||
|
||||
assert capture_log(fn ->
|
||||
|
@ -294,7 +317,9 @@ test "publish to url with with different ports" do
|
|||
ap_enabled: true
|
||||
})
|
||||
|
||||
actor = insert(:user, follower_address: follower.ap_id)
|
||||
actor =
|
||||
insert(:user, follower_address: follower.ap_id)
|
||||
|> with_signing_key()
|
||||
|
||||
{:ok, follower, actor} = Pleroma.User.follow(follower, actor)
|
||||
{:ok, _another_follower, actor} = Pleroma.User.follow(another_follower, actor)
|
||||
|
@ -365,7 +390,9 @@ test "publish to url with with different ports" do
|
|||
ap_enabled: true
|
||||
})
|
||||
|
||||
actor = insert(:user, follower_address: follower.ap_id)
|
||||
actor =
|
||||
insert(:user, follower_address: follower.ap_id)
|
||||
|> with_signing_key()
|
||||
|
||||
{:ok, follower, actor} = Pleroma.User.follow(follower, actor)
|
||||
actor = refresh_record(actor)
|
||||
|
|
|
@ -11,7 +11,9 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
|
|||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
test "Renders a user, including the public key" do
|
||||
user = insert(:user)
|
||||
user =
|
||||
insert(:user)
|
||||
|> with_signing_key()
|
||||
|
||||
result = UserView.render("user.json", %{user: user})
|
||||
|
||||
|
@ -37,7 +39,9 @@ test "Renders profile fields" do
|
|||
end
|
||||
|
||||
test "Renders with emoji tags" do
|
||||
user = insert(:user, emoji: %{"bib" => "/test"})
|
||||
user =
|
||||
insert(:user, emoji: %{"bib" => "/test"})
|
||||
|> with_signing_key()
|
||||
|
||||
assert %{
|
||||
"tag" => [
|
||||
|
@ -74,13 +78,18 @@ test "Does not add an avatar image if the user hasn't set one" do
|
|||
end
|
||||
|
||||
test "renders an invisible user with the invisible property set to true" do
|
||||
user = insert(:user, invisible: true)
|
||||
user =
|
||||
insert(:user, invisible: true)
|
||||
|> with_signing_key()
|
||||
|
||||
assert %{"invisible" => true} = UserView.render("service.json", %{user: user})
|
||||
end
|
||||
|
||||
test "service has a few essential fields" do
|
||||
user = insert(:user)
|
||||
user =
|
||||
insert(:user)
|
||||
|> with_signing_key()
|
||||
|
||||
result = UserView.render("service.json", %{user: user})
|
||||
assert result["id"]
|
||||
assert result["type"] == "Application"
|
||||
|
@ -120,7 +129,9 @@ test "remote users have an empty endpoints structure" do
|
|||
end
|
||||
|
||||
test "instance users do not expose oAuth endpoints" do
|
||||
user = insert(:user, nickname: nil, local: true)
|
||||
user =
|
||||
insert(:user, nickname: nil, local: true)
|
||||
|> with_signing_key()
|
||||
|
||||
result = UserView.render("user.json", %{user: user})
|
||||
|
||||
|
|
|
@ -14,6 +14,15 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
|
|||
import Phoenix.Controller, only: [put_format: 2]
|
||||
import Mock
|
||||
|
||||
setup do
|
||||
user =
|
||||
:user
|
||||
|> insert(%{ap_id: "http://mastodon.example.org/users/admin"})
|
||||
|> with_signing_key(%{key_id: "http://mastodon.example.org/users/admin#main-key"})
|
||||
|
||||
{:ok, %{user: user}}
|
||||
end
|
||||
|
||||
setup_with_mocks([
|
||||
{HTTPSignatures, [],
|
||||
[
|
||||
|
@ -46,15 +55,15 @@ defp submit_to_plug(host, method, path) do
|
|||
|> HTTPSignaturePlug.call(%{})
|
||||
end
|
||||
|
||||
test "it call HTTPSignatures to check validity if the actor signed it" do
|
||||
params = %{"actor" => "http://mastodon.example.org/users/admin"}
|
||||
test "it call HTTPSignatures to check validity if the actor signed it", %{user: user} do
|
||||
params = %{"actor" => user.ap_id}
|
||||
conn = build_conn(:get, "/doesntmattter", params)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header(
|
||||
"signature",
|
||||
"keyId=\"http://mastodon.example.org/users/admin#main-key"
|
||||
"keyId=\"#{user.signing_key.key_id}\""
|
||||
)
|
||||
|> put_format("activity+json")
|
||||
|> HTTPSignaturePlug.call(%{})
|
||||
|
|
|
@ -8,52 +8,63 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do
|
|||
|
||||
import Tesla.Mock
|
||||
import Plug.Conn
|
||||
import Pleroma.Factory
|
||||
|
||||
import Pleroma.Tests.Helpers, only: [clear_config: 2]
|
||||
|
||||
setup do
|
||||
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
||||
:ok
|
||||
|
||||
user =
|
||||
insert(:user)
|
||||
|> with_signing_key()
|
||||
|
||||
{:ok, %{user: user}}
|
||||
end
|
||||
|
||||
defp set_signature(conn, key_id) do
|
||||
defp set_signature(conn, ap_id) do
|
||||
conn
|
||||
|> put_req_header("signature", "keyId=\"#{key_id}\"")
|
||||
|> put_req_header("signature", "keyId=\"#{ap_id}#main-key\"")
|
||||
|> assign(:valid_signature, true)
|
||||
end
|
||||
|
||||
test "it successfully maps a valid identity with a valid signature" do
|
||||
test "it successfully maps a valid identity with a valid signature", %{user: user} do
|
||||
conn =
|
||||
build_conn(:get, "/doesntmattter")
|
||||
|> set_signature("http://mastodon.example.org/users/admin")
|
||||
|> set_signature(user.ap_id)
|
||||
|> MappedSignatureToIdentityPlug.call(%{})
|
||||
|
||||
refute is_nil(conn.assigns.user)
|
||||
end
|
||||
|
||||
test "it successfully maps a valid identity with a valid signature with payload" do
|
||||
test "it successfully maps a valid identity with a valid signature with payload", %{user: user} do
|
||||
conn =
|
||||
build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
|
||||
|> set_signature("http://mastodon.example.org/users/admin")
|
||||
build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id})
|
||||
|> set_signature(user.ap_id)
|
||||
|> MappedSignatureToIdentityPlug.call(%{})
|
||||
|
||||
refute is_nil(conn.assigns.user)
|
||||
end
|
||||
|
||||
test "it considers a mapped identity to be invalid when it mismatches a payload" do
|
||||
test "it considers a mapped identity to be invalid when it mismatches a payload", %{user: user} do
|
||||
conn =
|
||||
build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
|
||||
build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id})
|
||||
|> set_signature("https://niu.moe/users/rye")
|
||||
|> MappedSignatureToIdentityPlug.call(%{})
|
||||
|
||||
assert %{valid_signature: false} == conn.assigns
|
||||
end
|
||||
|
||||
test "it considers a mapped identity to be invalid when the associated instance is blocked" do
|
||||
test "it considers a mapped identity to be invalid when the associated instance is blocked", %{
|
||||
user: user
|
||||
} do
|
||||
clear_config([:activitypub, :authorized_fetch_mode], true)
|
||||
|
||||
# extract domain from user.ap_id
|
||||
url = URI.parse(user.ap_id)
|
||||
|
||||
clear_config([:mrf_simple, :reject], [
|
||||
{"mastodon.example.org", "anime is banned"}
|
||||
{url.host, "anime is banned"}
|
||||
])
|
||||
|
||||
on_exit(fn ->
|
||||
|
@ -62,18 +73,21 @@ test "it considers a mapped identity to be invalid when the associated instance
|
|||
end)
|
||||
|
||||
conn =
|
||||
build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
|
||||
|> set_signature("http://mastodon.example.org/users/admin")
|
||||
build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id})
|
||||
|> set_signature(user.ap_id)
|
||||
|> MappedSignatureToIdentityPlug.call(%{})
|
||||
|
||||
assert %{valid_signature: false} == conn.assigns
|
||||
end
|
||||
|
||||
test "allowlist federation: it considers a mapped identity to be valid when the associated instance is allowed" do
|
||||
test "allowlist federation: it considers a mapped identity to be valid when the associated instance is allowed",
|
||||
%{user: user} do
|
||||
clear_config([:activitypub, :authorized_fetch_mode], true)
|
||||
|
||||
url = URI.parse(user.ap_id)
|
||||
|
||||
clear_config([:mrf_simple, :accept], [
|
||||
{"mastodon.example.org", "anime is allowed"}
|
||||
{url.host, "anime is allowed"}
|
||||
])
|
||||
|
||||
on_exit(fn ->
|
||||
|
@ -82,15 +96,16 @@ test "allowlist federation: it considers a mapped identity to be valid when the
|
|||
end)
|
||||
|
||||
conn =
|
||||
build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
|
||||
|> set_signature("http://mastodon.example.org/users/admin")
|
||||
build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id})
|
||||
|> set_signature(user.ap_id)
|
||||
|> MappedSignatureToIdentityPlug.call(%{})
|
||||
|
||||
assert conn.assigns[:valid_signature]
|
||||
refute is_nil(conn.assigns.user)
|
||||
end
|
||||
|
||||
test "allowlist federation: it considers a mapped identity to be invalid when the associated instance is not allowed" do
|
||||
test "allowlist federation: it considers a mapped identity to be invalid when the associated instance is not allowed",
|
||||
%{user: user} do
|
||||
clear_config([:activitypub, :authorized_fetch_mode], true)
|
||||
|
||||
clear_config([:mrf_simple, :accept], [
|
||||
|
@ -103,8 +118,8 @@ test "allowlist federation: it considers a mapped identity to be invalid when th
|
|||
end)
|
||||
|
||||
conn =
|
||||
build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
|
||||
|> set_signature("http://mastodon.example.org/users/admin")
|
||||
build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id})
|
||||
|> set_signature(user.ap_id)
|
||||
|> MappedSignatureToIdentityPlug.call(%{})
|
||||
|
||||
assert %{valid_signature: false} == conn.assigns
|
||||
|
|
|
@ -47,7 +47,6 @@ def instance_factory(attrs \\ %{}) do
|
|||
end
|
||||
|
||||
def user_factory(attrs \\ %{}) do
|
||||
pem = Enum.random(@rsa_keys)
|
||||
# Argon2.hash_pwd_salt("test")
|
||||
# it really eats CPU time, so we use a precomputed hash
|
||||
password_hash =
|
||||
|
@ -64,8 +63,7 @@ def user_factory(attrs \\ %{}) do
|
|||
last_refreshed_at: NaiveDateTime.utc_now(),
|
||||
notification_settings: %Pleroma.User.NotificationSetting{},
|
||||
multi_factor_authentication_settings: %Pleroma.MFA.Settings{},
|
||||
ap_enabled: true,
|
||||
keys: pem
|
||||
ap_enabled: true
|
||||
}
|
||||
|
||||
urls =
|
||||
|
@ -97,6 +95,28 @@ def user_factory(attrs \\ %{}) do
|
|||
|> merge_attributes(attrs)
|
||||
end
|
||||
|
||||
def with_signing_key(%User{} = user, attrs \\ %{}) do
|
||||
signing_key =
|
||||
build(:signing_key, %{user: user, key_id: "#{user.ap_id}#main-key"})
|
||||
|> merge_attributes(attrs)
|
||||
|
||||
insert(signing_key)
|
||||
%{user | signing_key: signing_key}
|
||||
end
|
||||
|
||||
def signing_key_factory(attrs \\ %{}) do
|
||||
pem = Enum.random(@rsa_keys)
|
||||
user = attrs[:user] || insert(:user)
|
||||
{:ok, public_key} = Pleroma.User.SigningKey.private_pem_to_public_pem(pem)
|
||||
|
||||
%Pleroma.User.SigningKey{
|
||||
user_id: user.id,
|
||||
public_key: attrs[:public_key] || public_key,
|
||||
private_key: attrs[:private_key] || pem,
|
||||
key_id: attrs[:key_id]
|
||||
}
|
||||
end
|
||||
|
||||
def user_relationship_factory(attrs \\ %{}) do
|
||||
source = attrs[:source] || insert(:user)
|
||||
target = attrs[:target] || insert(:user)
|
||||
|
|
|
@ -66,6 +66,8 @@ defmacro __using__(_opts) do
|
|||
clear_config: 2
|
||||
]
|
||||
|
||||
import Pleroma.Test.MatchingHelpers
|
||||
|
||||
def time_travel(entity, seconds) do
|
||||
new_time = NaiveDateTime.add(entity.inserted_at, seconds)
|
||||
|
||||
|
|
|
@ -424,6 +424,15 @@ def get("http://mastodon.example.org/users/admin", _, _, _) do
|
|||
}}
|
||||
end
|
||||
|
||||
def get("http://mastodon.example.org/users/admin/main-key", _, _, _) do
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body: File.read!("test/fixtures/tesla_mock/admin@mastdon.example.org.json"),
|
||||
headers: activitypub_object_headers()
|
||||
}}
|
||||
end
|
||||
|
||||
def get(
|
||||
"http://mastodon.example.org/users/admin/statuses/99512778738411822/replies?min_id=99512778738411824&page=true",
|
||||
_,
|
||||
|
@ -958,6 +967,15 @@ def get("https://mastodon.social/users/lambadalambda", _, _, _) do
|
|||
}}
|
||||
end
|
||||
|
||||
def get("https://mastodon.social/users/lambadalambda#main-key", _, _, _) do
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body: File.read!("test/fixtures/lambadalambda.json"),
|
||||
headers: activitypub_object_headers()
|
||||
}}
|
||||
end
|
||||
|
||||
def get("https://mastodon.social/users/lambadalambda/collections/featured", _, _, _) do
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
|
@ -1403,6 +1421,15 @@ def get("https://relay.mastodon.host/actor", _, _, _) do
|
|||
}}
|
||||
end
|
||||
|
||||
def get("https://relay.mastodon.host/actor#main-key", _, _, _) do
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body: File.read!("test/fixtures/relay/relay.json"),
|
||||
headers: activitypub_object_headers()
|
||||
}}
|
||||
end
|
||||
|
||||
def get("http://localhost:4001/", _, "", [{"accept", "text/html"}]) do
|
||||
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/7369654.html")}}
|
||||
end
|
||||
|
|
10
test/support/matching_helpers.ex
Normal file
|
@ -0,0 +1,10 @@
|
|||
defmodule Pleroma.Test.MatchingHelpers do
|
||||
import ExUnit.Assertions
|
||||
|
||||
@assoc_fields [
|
||||
:signing_key
|
||||
]
|
||||
def assert_user_match(actor1, actor2) do
|
||||
assert Ecto.reset_fields(actor1, @assoc_fields) == Ecto.reset_fields(actor2, @assoc_fields)
|
||||
end
|
||||
end
|
comment doesn't match the visible code beneath it
Comment implies the current function will fall back to something else if db lookup fails, but only attempts one function call