diff --git a/lib/nulla/httpsignature.ex b/lib/nulla/httpsignature.ex index 8831606..9183604 100644 --- a/lib/nulla/httpsignature.ex +++ b/lib/nulla/httpsignature.ex @@ -1,5 +1,70 @@ defmodule Nulla.HTTPSignature do - def verify(_conn, _actor) do - :ok + import Plug.Conn + + def verify(conn, actor_json) do + with [sig_header] <- get_req_header(conn, "signature"), + signature_map <- parse_signature_header(sig_header), + {:ok, signed_string} <- build_signature_string(signature_map["headers"], conn), + {:ok, public_key_pem} <- extract_public_key(actor_json), + true <- verify_signature(public_key_pem, signed_string, signature_map["signature"]) do + :ok + else + _ -> {:error, :invalid_signature} + end + end + + defp parse_signature_header(header) do + header + |> String.split(",") + |> Enum.map(fn pair -> + [k, v] = String.split(pair, "=", parts: 2) + {String.trim(k), String.trim(v, ~s("))} + end) + |> Enum.into(%{}) + end + + defp build_signature_string(nil, _conn), do: {:error, :missing_headers} + + defp build_signature_string(headers_str, conn) do + headers = String.split(headers_str, " ") + + result = + Enum.map(headers, fn header -> + line = + case header do + "(request-target)" -> + method = String.downcase(conn.method) + + path = + conn.request_path <> + if conn.query_string != "", do: "?" <> conn.query_string, else: "" + + "(request-target): #{method} #{path}" + + _ -> + value = get_req_header(conn, header) |> List.first() + if value, do: "#{header}: #{value}", else: nil + end + + line + end) + |> Enum.reject(&is_nil/1) + |> Enum.join("\n") + + {:ok, result} + end + + defp extract_public_key(%{"publicKey" => %{"publicKeyPem" => pem}}), do: {:ok, pem} + defp extract_public_key(_), do: {:error, :no_public_key} + + defp verify_signature(public_key_pem, signed_string, signature_base64) do + public_key = + :public_key.pem_decode(public_key_pem) + |> hd() + |> :public_key.pem_entry_decode() + + signature = Base.decode64!(signature_base64) + + :public_key.verify(signed_string, :sha256, signature, public_key) end end diff --git a/lib/nulla/models/actor.ex b/lib/nulla/models/actor.ex index a4f579c..e00929b 100644 --- a/lib/nulla/models/actor.ex +++ b/lib/nulla/models/actor.ex @@ -138,7 +138,7 @@ defmodule Nulla.Models.Actor do |> Repo.insert() end - def get_actor(username, domain) do - Repo.get_by(__MODULE__, preferredUsername: username, domain: domain) + def get_actor(by) when is_map(by) or is_list(by) do + Repo.get_by(__MODULE__, by) end end diff --git a/lib/nulla/models/user.ex b/lib/nulla/models/user.ex index 4bb596e..9c5adeb 100644 --- a/lib/nulla/models/user.ex +++ b/lib/nulla/models/user.ex @@ -3,7 +3,6 @@ defmodule Nulla.Models.User do import Ecto.Changeset import Ecto.Query alias Nulla.Repo - alias Nulla.Models.User alias Nulla.Models.Session @primary_key {:id, :integer, autogenerate: false} @@ -44,17 +43,17 @@ defmodule Nulla.Models.User do end def get_user(by) when is_map(by) or is_list(by) do - Repo.get_by(User, by) + Repo.get_by(__MODULE__, by) end def get_total_users_count() do - Repo.aggregate(from(u in User), :count, :id) + Repo.aggregate(from(u in __MODULE__), :count, :id) end def get_active_users_count(days) do cutoff = DateTime.add(DateTime.utc_now(), -days * 86400, :second) - from(u in User, where: u.last_active_at > ^cutoff) + from(u in __MODULE__, where: u.last_active_at > ^cutoff) |> Repo.aggregate(:count, :id) end diff --git a/lib/nulla/utils.ex b/lib/nulla/utils.ex index 4e74f08..a0454d7 100644 --- a/lib/nulla/utils.ex +++ b/lib/nulla/utils.ex @@ -1,28 +1,12 @@ defmodule Nulla.Utils do - alias Nulla.Models.Actor - alias Nulla.Models.InstanceSettings - - def resolve_local_actor("https://" <> _ = uri) do - case URI.parse(uri).path do - "/@" <> username -> - instance_settings = InstanceSettings.get_instance_settings!() - domain = instance_settings.domain - - case Actor.get_actor(username, domain) do - nil -> {:error, :not_found} - user -> user - end - - _ -> - {:error, :invalid_actor} - end - end - def fetch_remote_actor(uri) do - request = - Finch.build(:get, uri, [ - {"Accept", "application/activity+json"} - ]) + headers = [ + {"Accept", "application/activity+json"}, + {"User-Agent", "Nulla/1.0"}, + {"Host", URI.parse(uri).host} + ] + + request = Finch.build(:get, uri, headers) case Finch.request(request, Finch) do {:ok, %Finch.Response{status: 200, body: body}} -> @@ -31,6 +15,9 @@ defmodule Nulla.Utils do _ -> {:error, :invalid_json} end + {:ok, %Finch.Response{status: code}} when code in 300..399 -> + {:error, :redirect_not_followed} + _ -> {:error, :actor_fetch_failed} end diff --git a/lib/nulla_web/controllers/actor_controller.ex b/lib/nulla_web/controllers/actor_controller.ex index e497888..c4b8479 100644 --- a/lib/nulla_web/controllers/actor_controller.ex +++ b/lib/nulla_web/controllers/actor_controller.ex @@ -10,7 +10,7 @@ defmodule NullaWeb.ActorController do instance_settings = InstanceSettings.get_instance_settings!() domain = instance_settings.domain - case Actor.get_actor(username, domain) do + case Actor.get_actor(preferredUsername: username, domain: domain) do nil -> conn |> put_status(:not_found) diff --git a/lib/nulla_web/controllers/follow_controller.ex b/lib/nulla_web/controllers/follow_controller.ex index ca5afee..8a21c7d 100644 --- a/lib/nulla_web/controllers/follow_controller.ex +++ b/lib/nulla_web/controllers/follow_controller.ex @@ -9,7 +9,7 @@ defmodule NullaWeb.FollowController do instance_settings = InstanceSettings.get_instance_settings!() domain = instance_settings.domain limit = instance_settings.api_limit - actor = Actor.get_actor(username, domain) + actor = Actor.get_actor(preferredUsername: username, domain: domain) total = Relation.count_following(actor.id) page = @@ -28,7 +28,7 @@ defmodule NullaWeb.FollowController do def following(conn, %{"username" => username}) do instance_settings = InstanceSettings.get_instance_settings!() domain = instance_settings.domain - actor = Actor.get_actor(username, domain) + actor = Actor.get_actor(preferredUsername: username, domain: domain) total = Relation.count_following(actor.id) conn @@ -40,7 +40,7 @@ defmodule NullaWeb.FollowController do instance_settings = InstanceSettings.get_instance_settings!() domain = instance_settings.domain limit = instance_settings.api_limit - actor = Actor.get_actor(username, domain) + actor = Actor.get_actor(preferredUsername: username, domain: domain) total = Relation.count_followers(actor.id) page = @@ -59,7 +59,7 @@ defmodule NullaWeb.FollowController do def followers(conn, %{"username" => username}) do instance_settings = InstanceSettings.get_instance_settings!() domain = instance_settings.domain - actor = Actor.get_actor(username, domain) + actor = Actor.get_actor(preferredUsername: username, domain: domain) total = Relation.count_followers(actor.id) conn diff --git a/lib/nulla_web/controllers/inbox_controller.ex b/lib/nulla_web/controllers/inbox_controller.ex index 06f3689..a58ffec 100644 --- a/lib/nulla_web/controllers/inbox_controller.ex +++ b/lib/nulla_web/controllers/inbox_controller.ex @@ -11,7 +11,7 @@ defmodule NullaWeb.InboxController do conn, %{"id" => follow_id, "type" => "Follow", "actor" => actor_uri, "object" => target_uri} ) do - with {:ok, local_actor} <- Utils.resolve_local_actor(target_uri), + with local_actor <- Actor.get_actor(ap_id: target_uri), {:ok, remote_actor_json} <- Utils.fetch_remote_actor(actor_uri), :ok <- HTTPSignature.verify(conn, remote_actor_json), remote_actor <- @@ -31,7 +31,7 @@ defmodule NullaWeb.InboxController do accept_activity <- Activity.create_activity(%{ type: "Accept", - actor: local_actor.id, + actor: local_actor.ap_id, object: follow_activity }), _ <- diff --git a/lib/nulla_web/controllers/outbox_controller.ex b/lib/nulla_web/controllers/outbox_controller.ex index dffa3d8..093015d 100644 --- a/lib/nulla_web/controllers/outbox_controller.ex +++ b/lib/nulla_web/controllers/outbox_controller.ex @@ -10,7 +10,7 @@ defmodule NullaWeb.OutboxController do "true" -> instance_settings = InstanceSettings.get_instance_settings!() domain = instance_settings.domain - actor = Actor.get_actor(username, domain) + actor = Actor.get_actor(preferredUsername: username, domain: domain) max_id = params["max_id"] && String.to_integer(params["max_id"]) notes = @@ -44,7 +44,7 @@ defmodule NullaWeb.OutboxController do _ -> instance_settings = InstanceSettings.get_instance_settings!() domain = instance_settings.domain - actor = Actor.get_actor(username, domain) + actor = Actor.get_actor(preferredUsername: username, domain: domain) total = Note.get_total_notes_count(actor.id) conn diff --git a/lib/nulla_web/controllers/webfinger_controller.ex b/lib/nulla_web/controllers/webfinger_controller.ex index a36e6b3..3db0a28 100644 --- a/lib/nulla_web/controllers/webfinger_controller.ex +++ b/lib/nulla_web/controllers/webfinger_controller.ex @@ -7,7 +7,7 @@ defmodule NullaWeb.WebfingerController do def index(conn, %{"resource" => resource}) do case Regex.run(~r/^acct:(.+)@(.+)$/, resource) do [_, username, domain] -> - case Actor.get_actor(username, domain) do + case Actor.get_actor(preferredUsername: username, domain: domain) do nil -> conn |> put_resp_content_type("text/plain")