diff --git a/lib/vonbraun/activitypub_req.ex b/lib/vonbraun/activitypub_req.ex index 339d464..22cb348 100644 --- a/lib/vonbraun/activitypub_req.ex +++ b/lib/vonbraun/activitypub_req.ex @@ -32,7 +32,8 @@ defmodule Vonbraun.ActivityPubReq do Req.get(url, headers: headers) end - def post(url = %URI{:path => path, :query => query, :host => host}, body) when is_binary(body) do + def post(url = %URI{:path => path, :query => query, :host => host}, body) + when is_binary(body) do headers = %{ "host" => host, "accept" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" diff --git a/lib/vonbraun/control.ex b/lib/vonbraun/control.ex index 0c7f35a..504bbc0 100644 --- a/lib/vonbraun/control.ex +++ b/lib/vonbraun/control.ex @@ -1,6 +1,7 @@ defmodule Vonbraun.Control do require Logger alias Vonbraun.ActivityPubReq + alias Vonbraun.Ecto.Schema.Actor defp self_id() do domain = Application.fetch_env!(:vonbraun, :domain) @@ -8,10 +9,20 @@ defmodule Vonbraun.Control do "https://#{domain}/users/#{nickname}" end + @spec follow(String.t()) :: + :ok + | {:error, + :blocked + | :invalid_inbox + | {:actor, {any(), any()} | map()} + | {:post, map()} + | {:status, non_neg_integer()}} def follow(followee_actor_id) when is_binary(followee_actor_id) do with {:actor, {:ok, actor}} <- {:actor, ActivityPubReq.get_actor(followee_actor_id)}, {:inbox, {:ok, raw_inbox}} <- {:inbox, ActivityPubReq.extract_actor_inbox(actor)}, - {:parse_inbox, inbox = %URI{}} <- {:parse_inbox, URI.parse(raw_inbox)} do + {:parse_inbox, inbox = %URI{}} <- {:parse_inbox, URI.parse(raw_inbox)}, + {:pending, {:ok, %{:blocks_me => nil, :following_state => "pending"}}} <- + {:pending, Actor.mark_pending_follow(followee_actor_id, "pending")} do domain = Application.fetch_env!(:vonbraun, :domain) # Is it a bad idea making this absolute? activity_id = "https://#{domain}/id/follow:#{URI.encode(followee_actor_id)}" @@ -41,11 +52,32 @@ defmodule Vonbraun.Control do {:ok, %{:status => status, :body => body}} -> Logger.debug(inspect(body)) Logger.warning("Got a status of: #{status}, probably not good.") + {:error, {:status, status}} {:error, error} -> Logger.error("Failed to send follow request: #{inspect(error)}") - {:error, error} + {:error, {:post, error}} end + else + {:pending, {:ok, %{:blocks_me => nil, :following_state => "accepted"}}} -> + Logger.info("I think you already follow them.") + :ok + + {:pending, {:ok, %{:blocks_me => nil, :following_state => "rejected"}}} -> + Logger.warning("I think they already rejected your follow") + :ok + + {:actor, {:error, error}} -> + Logger.warning("Failed to query remote actor: #{inspect(error)}") + {:error, {:actor, error}} + + {:pending, {:ok, %{:blocks_me => _}}} -> + Logger.info("This user blocks you.") + {:error, :blocked} + + {:inbox, {:error, _}} -> + Logger.warning("Actor had an invalid inbox.") + {:error, :invalid_inbox} end end diff --git a/lib/vonbraun/ecto/schema/actor.ex b/lib/vonbraun/ecto/schema/actor.ex index bc697e3..e25ac71 100644 --- a/lib/vonbraun/ecto/schema/actor.ex +++ b/lib/vonbraun/ecto/schema/actor.ex @@ -169,20 +169,24 @@ defmodule Vonbraun.Ecto.Schema.Actor do @spec get_my_followers() :: list(String.t()) def get_my_followers() do - query = from u in __MODULE__, - where: u.follows_me_state == "accepted", - select: u.id, - order_by: u.follows_me_ts + query = + from(u in __MODULE__, + where: u.follows_me_state == "accepted", + select: u.id, + order_by: u.follows_me_ts + ) Repo.all(query) end @spec get_my_follows() :: list(String.t()) def get_my_follows() do - query = from u in __MODULE__, - where: u.following_state == "accepted", - select: u.id, - order_by: u.following_ts + query = + from(u in __MODULE__, + where: u.following_state == "accepted", + select: u.id, + order_by: u.following_ts + ) Repo.all(query) end diff --git a/lib/vonbraun/fluree.ex b/lib/vonbraun/fluree.ex index 8c1c516..7447f6b 100644 --- a/lib/vonbraun/fluree.ex +++ b/lib/vonbraun/fluree.ex @@ -53,21 +53,27 @@ defmodule Vonbraun.Fluree do i_follow? = Keyword.get(options, :following, false) props = Map.new() - props = if me? do - Map.put(props, "me", true) - else - props - end - props = if is_follower? do - Map.put(props, "isFollower", true) - else - props - end - props = if i_follow? do - Map.put(props, "following", true) - else - props - end + + props = + if me? do + Map.put(props, "me", true) + else + props + end + + props = + if is_follower? do + Map.put(props, "isFollower", true) + else + props + end + + props = + if i_follow? do + Map.put(props, "following", true) + else + props + end Map.put(actor, "vonbraun:props", props) diff --git a/lib/vonbraun/http_signature.ex b/lib/vonbraun/http_signature.ex index 50d8e9d..7da22ba 100644 --- a/lib/vonbraun/http_signature.ex +++ b/lib/vonbraun/http_signature.ex @@ -62,65 +62,70 @@ defmodule Vonbraun.HTTPSignature do valid_request_target = "post #{target}" with {:raw_body, {:ok, raw_body, _}} <- {:raw_body, Conn.read_body(conn)}, - {:signature_header, [signature_header]} <- + {:signature_header, [signature_header]} <- {:signature_header, Conn.get_req_header(conn, "signature")}, {:parse, - {:ok, %{ - "keyId" => _key_id, - "headers" => signing_headers, - "signature" => {:ok, signature} - } - }} <- {:parse, parse_signature_header(signature_header)} do - # Reconstruct the signing data - reconstructed_header_list = Enum.reduce_while(signing_headers |> Enum.reverse(), [], fn - "(request-target)", list -> - {:cont, ["(request-target): #{valid_request_target}" | list]} + {:ok, + %{ + "keyId" => _key_id, + "headers" => signing_headers, + "signature" => {:ok, signature} + }}} <- {:parse, parse_signature_header(signature_header)} do + # Reconstruct the signing data + reconstructed_header_list = + Enum.reduce_while(signing_headers |> Enum.reverse(), [], fn + "(request-target)", list -> + {:cont, ["(request-target): #{valid_request_target}" | list]} - "digest", list -> - with {:get, [header_value]} <- {:get, Conn.get_req_header(conn, "digest")}, - {:split, [digest_algo, encoded_digest]} <- {:split, String.split(header_value, "=", parts: 2)}, - {:decode, {:ok, provided_digest}} <- {:decode, Base.decode64(encoded_digest)} do - new_digest = :crypto.hash(:sha256, raw_body) - test_digest = digest_algo <> "=" <> (new_digest |> Base.encode64()) + "digest", list -> + with {:get, [header_value]} <- {:get, Conn.get_req_header(conn, "digest")}, + {:split, [digest_algo, encoded_digest]} <- + {:split, String.split(header_value, "=", parts: 2)}, + {:decode, {:ok, provided_digest}} <- {:decode, Base.decode64(encoded_digest)} do + new_digest = :crypto.hash(:sha256, raw_body) + test_digest = digest_algo <> "=" <> (new_digest |> Base.encode64()) - if provided_digest == new_digest do - {:cont, ["digest: #{test_digest}" | list]} - else - Logger.warning("digest failed: #{provided_digest} <> #{test_digest}") - {:halt, false} - end - else - error -> - Logger.warning("Failed to handle digest: #{inspect(error)}") - {:halt, false} - end - - header_name, list -> - case Conn.get_req_header(conn, header_name) do - [] -> - Logger.warning("header not found for http signature verification: #{header_name}") - {:halt, false} - - [header_value] -> - {:cont, ["#{header_name}: #{header_value}" | list]} - end - end) - - case reconstructed_header_list do - false -> false - - _ -> - # Hope they use the same newline chars - test_str = Enum.join(reconstructed_header_list, "\n") - Logger.debug("Here is the test string for signature verification: `#{test_str}`") - - case ExPublicKey.verify(test_str, signature, public_key) do - {:ok, val} -> val - error -> - Logger.warning("Error verifying signature: #{inspect(error)}") - false + if provided_digest == new_digest do + {:cont, ["digest: #{test_digest}" | list]} + else + Logger.warning("digest failed: #{provided_digest} <> #{test_digest}") + {:halt, false} end + else + error -> + Logger.warning("Failed to handle digest: #{inspect(error)}") + {:halt, false} + end + + header_name, list -> + case Conn.get_req_header(conn, header_name) do + [] -> + Logger.warning("header not found for http signature verification: #{header_name}") + {:halt, false} + + [header_value] -> + {:cont, ["#{header_name}: #{header_value}" | list]} + end + end) + + case reconstructed_header_list do + false -> + false + + _ -> + # Hope they use the same newline chars + test_str = Enum.join(reconstructed_header_list, "\n") + Logger.debug("Here is the test string for signature verification: `#{test_str}`") + + case ExPublicKey.verify(test_str, signature, public_key) do + {:ok, val} -> + val + + error -> + Logger.warning("Error verifying signature: #{inspect(error)}") + false end + end else error -> Logger.warning("Verify http sig unmatch: #{inspect(error)}") diff --git a/lib/vonbraun/inbox_router.ex b/lib/vonbraun/inbox_router.ex index a2497c5..695e165 100644 --- a/lib/vonbraun/inbox_router.ex +++ b/lib/vonbraun/inbox_router.ex @@ -44,13 +44,16 @@ defmodule Vonbraun.InboxRouter do :rejected -> 200 + + _ -> + 200 end send_resp(conn, status_code, "boop") else - error -> + {:error, error} -> Logger.warning("Some kind of failure: #{inspect(error)}") - send_resp(conn, 401, "fuck off") + send_resp(conn, 500, "I fucked up") end true -> @@ -58,10 +61,34 @@ defmodule Vonbraun.InboxRouter do end end + defp extract_follow_object_actor(%{"type" => "Follow", "actor" => actor_id}) + when is_binary(actor_id) do + {:ok, actor_id} + end + + defp extract_follow_object_actor(%{"type" => "Follow", "id" => provided_follow_activity_id}) + when is_binary(provided_follow_activity_id) do + domain = Application.fetch_env!(:vonbraun, :domain) + actual_activity_id_prefix = "https://#{domain}/id/follow:" + + with ^actual_activity_id_prefix <> actor_id <- provided_follow_activity_id do + {:ok, actor_id} + else + _ -> + {:error, :notfound} + end + end + + defp extract_follow_object_actor(_) do + {:error, :notfound} + end + + # TODO: make sure I'm in the to list. defp handle_activity( %{"type" => "Follow", "actor" => actor_id, "object" => follow_target}, actor = %{} - ) do + ) + when is_binary(follow_target) do with {:valid_target, true} <- {:valid_target, ActivityPubReq.actor_id() == follow_target}, {:add, {:ok, %Actor{:blocked => nil, :follows_me_state => follows_me_state}}} when not is_nil(follows_me_state) <- {:add, Actor.maybe_add_follower(actor_id)} do @@ -103,10 +130,12 @@ defmodule Vonbraun.InboxRouter do case ActivityPubReq.post(inbox, payload) do {:ok, %{:status => status, :body => body}} -> Logger.debug("Accept response status: #{status} body: #{inspect(body)}") + {:error, error} -> Logger.error("Failed to Accept: #{inspect(error)}") end end) + {:ok, String.to_atom(follows_me_state)} else {:inbox, {:error, _}} -> @@ -131,7 +160,45 @@ defmodule Vonbraun.InboxRouter do end end - defp handle_activity(_activity = %{}, _) do + defp handle_activity( + %{ + "type" => "Accept", + "actor" => actor_id, + "object" => object = %{"type" => "Follow"} + }, + _actor = %{} + ) + when is_binary(actor_id) do + with {:asked, {:ok, %Actor{:blocked => nil, :following_state => "accepted"}}} <- + {:asked, Actor.mark_pending_follow(actor_id, "accepted", force: true)}, + {:actor, {:ok, follow_actor_id}} <- {:actor, extract_follow_object_actor(object)}, + {:match, true} <- {:match, follow_actor_id == ActivityPubReq.actor_id()} do + Logger.info("Now following: #{actor_id}") + {:ok, :following} + else + {:asked, {:error, :blocked}} -> + {:ok, :blocked_user} + + {:asked, {:error, error}} -> + {:error, error} + + {:asked, {:ok, %Actor{:blocked => nil, :following_state => following_state}}} -> + Logger.error( + "Weird following state after received Accept: #{following_state} from actor: #{actor_id} this should not happen." + ) + + {:error, :following_state} + + {:actor, {:error, _error}} -> + {:ok, :unauthorized} + + {:match, false} -> + {:ok, :unauthorized} + end + end + + defp handle_activity(activity = %{}, _) do + Logger.warning("I don't know what to do with it: #{inspect(activity)}") {:ok, :ignored} end end