Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2018-05-18 09:56:21 +02:00
parent e14007bac5
commit f1cb601b46
39 changed files with 1289 additions and 1028 deletions

View File

@@ -5,8 +5,8 @@ defmodule Eventos.Service.ActivityPub do
alias Eventos.Service.WebFinger
alias Eventos.Activity
alias Eventos.Accounts
alias Eventos.Accounts.Account
alias Eventos.Actors
alias Eventos.Actors.Actor
alias Eventos.Service.Federator
@@ -83,8 +83,12 @@ defmodule Eventos.Service.ActivityPub do
),
{:ok, activity} <- insert(create_data, local),
:ok <- maybe_federate(activity) do
# {:ok, actor} <- Accounts.increase_event_count(actor) do
# {:ok, actor} <- Actors.increase_event_count(actor) do
{:ok, activity}
else
err ->
Logger.debug("Something went wrong")
Logger.debug(inspect err)
end
end
@@ -124,13 +128,13 @@ defmodule Eventos.Service.ActivityPub do
end
end
def delete(%Event{url: url, organizer_account: account} = event, local \\ true) do
def delete(%Event{url: url, organizer_actor: actor} = event, local \\ true) do
data = %{
"type" => "Delete",
"actor" => account.url,
"actor" => actor.url,
"object" => url,
"to" => [account.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
"to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
}
with Events.delete_event(event),
@@ -141,40 +145,43 @@ defmodule Eventos.Service.ActivityPub do
end
end
def create_public_activities(%Account{} = account) do
def create_public_activities(%Actor{} = actor) do
end
def make_account_from_url(url) do
def make_actor_from_url(url) do
with {:ok, data} <- fetch_and_prepare_user_from_url(url) do
Accounts.insert_or_update_account(data)
Actors.insert_or_update_actor(data)
else
e ->
Logger.error("Failed to make account from url")
Logger.error("Failed to make actor from url")
Logger.error(inspect e)
{:error, e}
end
end
def make_account_from_nickname(nickname) do
def make_actor_from_nickname(nickname) do
with {:ok, %{"url" => url}} when not is_nil(url) <- WebFinger.finger(nickname) do
make_account_from_url(url)
make_actor_from_url(url)
else
_e -> {:error, "No ActivityPub URL found in WebFinger"}
end
end
def publish(actor, activity) do
# followers =
# if actor.follower_address in activity.recipients do
# {:ok, followers} = User.get_followers(actor)
# followers |> Enum.filter(&(!&1.local))
# else
# []
# end
followers = ["http://localhost:3000/users/tcit/inbox"]
Logger.debug("Publishing an activity")
followers =
if actor.followers_url in activity.recipients do
{:ok, followers} = Actor.get_followers(actor)
followers |> Enum.filter(fn follower -> is_nil(follower.domain) end)
else
[]
end
remote_inboxes = followers
remote_inboxes =
followers
|> Enum.map(fn follower -> follower.shared_inbox_url end)
|> Enum.uniq()
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
json = Jason.encode!(data)
@@ -219,6 +226,8 @@ defmodule Eventos.Service.ActivityPub do
end
def user_data_from_user_object(data) do
Logger.debug("user_data_from_user_object")
Logger.debug(inspect data)
avatar =
data["icon"]["url"] &&
%{
@@ -241,19 +250,27 @@ defmodule Eventos.Service.ActivityPub do
"banner" => banner
},
avatar: avatar,
username: "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}",
display_name: data["name"],
name: data["name"],
preferred_username: data["preferredUsername"],
follower_address: data["followers"],
description: data["summary"],
summary: data["summary"],
public_key: data["publicKey"]["publicKeyPem"],
inbox_url: data["inbox"],
outbox_url: data["outbox"],
following_url: data["following"],
followers_url: data["followers"],
shared_inbox_url: data["sharedInbox"],
domain: URI.parse(data["id"]).host,
manually_approves_followers: data["manuallyApprovesFollowers"],
type: data["type"],
}
{:ok, user_data}
end
@spec fetch_public_activities_for_account(Account.t, integer(), integer()) :: list()
def fetch_public_activities_for_account(%Account{} = account, page \\ 10, limit \\ 1) do
{:ok, events, total} = Events.get_events_for_account(account, page, limit)
@spec fetch_public_activities_for_actor(Actor.t, integer(), integer()) :: list()
def fetch_public_activities_for_actor(%Actor{} = actor, page \\ 10, limit \\ 1) do
{:ok, events, total} = Events.get_events_for_actor(actor, page, limit)
activities = Enum.map(events, fn event ->
{:ok, activity} = event_to_activity(event)
activity
@@ -265,7 +282,7 @@ defmodule Eventos.Service.ActivityPub do
activity = %Activity{
data: event,
local: true,
actor: event.organizer_account.url,
actor: event.organizer_actor.url,
recipients: ["https://www.w3.org/ns/activitystreams#Public"]
}

View File

@@ -2,8 +2,8 @@ defmodule Eventos.Service.ActivityPub.Transmogrifier do
@moduledoc """
A module to handle coding from internal to wire ActivityPub and back.
"""
alias Eventos.Accounts.Account
alias Eventos.Accounts
alias Eventos.Actors.Actor
alias Eventos.Actors
alias Eventos.Events.Event
alias Eventos.Service.ActivityPub
@@ -101,13 +101,15 @@ defmodule Eventos.Service.ActivityPub.Transmogrifier do
# - tags
# - emoji
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do
with %Account{} = account <- Account.get_or_fetch_by_url(data["actor"]) do
Logger.debug("Handle incoming to create notes")
with %Actor{} = actor <- Actor.get_or_fetch_by_url(data["actor"]) do
Logger.debug("found actor")
object = fix_object(data["object"])
params = %{
to: data["to"],
object: object,
actor: account,
actor: actor,
context: object["conversation"],
local: false,
published: data["published"],
@@ -122,15 +124,13 @@ defmodule Eventos.Service.ActivityPub.Transmogrifier do
end
end
def handle_incoming(
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
) do
with %Account{} = followed <- Accounts.get_account_by_url(followed),
%Account{} = follower <- Accounts.get_or_fetch_by_url(follower),
def handle_incoming(%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data) do
with %Actor{} = followed <- Actors.get_actor_by_url(followed),
%Actor{} = follower <- Actors.get_or_fetch_by_url(follower),
{:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
ActivityPub.accept(%{to: [follower.url], actor: followed.url, object: data, local: true})
#Accounts.follow(follower, followed)
#Actors.follow(follower, followed)
{:ok, activity}
else
_e -> :error

View File

@@ -1,7 +1,7 @@
defmodule Eventos.Service.ActivityPub.Utils do
alias Eventos.Repo
alias Eventos.Accounts
alias Eventos.Accounts.Account
alias Eventos.Actors
alias Eventos.Actors.Actor
alias Eventos.Events.Event
alias Eventos.Events
alias Eventos.Activity
@@ -126,8 +126,11 @@ defmodule Eventos.Service.ActivityPub.Utils do
"""
def insert_full_object(%{"object" => %{"type" => type} = object_data})
when is_map(object_data) and type == "Note" do
account = Accounts.get_account_by_url(object_data["actor"])
data = %{"text" => object_data["content"], "url" => object_data["url"], "account_id" => account.id, "in_reply_to_comment_id" => object_data["inReplyTo"]}
import Logger
Logger.debug("insert full object")
Logger.debug(inspect object_data)
actor = Actors.get_actor_by_url(object_data["actor"])
data = %{"text" => object_data["content"], "url" => object_data["id"], "actor_id" => actor.id, "in_reply_to_comment_id" => object_data["inReplyTo"]}
with {:ok, _} <- Events.create_comment(data) do
:ok
end
@@ -173,7 +176,7 @@ defmodule Eventos.Service.ActivityPub.Utils do
# Repo.one(query)
# end
def make_like_data(%Account{url: url} = actor, %{data: %{"id" => id}} = object, activity_id) do
def make_like_data(%Actor{url: url} = actor, %{data: %{"id" => id}} = object, activity_id) do
data = %{
"type" => "Like",
"actor" => url,
@@ -218,7 +221,7 @@ defmodule Eventos.Service.ActivityPub.Utils do
@doc """
Makes a follow activity data for the given follower and followed
"""
def make_follow_data(%Account{url: follower_id}, %Account{url: followed_id}, activity_id) do
def make_follow_data(%Actor{url: follower_id}, %Actor{url: followed_id}, activity_id) do
data = %{
"type" => "Follow",
"actor" => follower_id,
@@ -230,7 +233,7 @@ defmodule Eventos.Service.ActivityPub.Utils do
if activity_id, do: Map.put(data, "id", activity_id), else: data
end
# def fetch_latest_follow(%Account{url: follower_id}, %Account{url: followed_id}) do
# def fetch_latest_follow(%Actor{url: follower_id}, %Actor{url: followed_id}) do
# query =
# from(
# activity in Activity,
@@ -253,7 +256,7 @@ defmodule Eventos.Service.ActivityPub.Utils do
Make announce activity data for the given actor and object
"""
def make_announce_data(
%Account{url: url} = user,
%Actor{url: url} = user,
%Event{id: id} = object,
activity_id
) do

View File

@@ -1,6 +1,6 @@
defmodule Eventos.Service.Federator do
use GenServer
alias Eventos.Accounts
alias Eventos.Actors
alias Eventos.Activity
alias Eventos.Service.ActivityPub
alias Eventos.Service.ActivityPub.Transmogrifier
@@ -33,7 +33,7 @@ defmodule Eventos.Service.Federator do
Logger.debug(inspect activity)
Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
with actor when not is_nil(actor) <- Accounts.get_account_by_url(activity.data["actor"]) do
with actor when not is_nil(actor) <- Actors.get_actor_by_url(activity.data["actor"]) do
Logger.info(fn -> "Sending #{activity.data["id"]} out via AP" end)
ActivityPub.publish(actor, activity)

View File

@@ -1,6 +1,6 @@
# https://tools.ietf.org/html/draft-cavage-http-signatures-08
defmodule Eventos.Service.HTTPSignatures do
alias Eventos.Accounts.Account
alias Eventos.Actors.Actor
alias Eventos.Service.ActivityPub
require Logger
@@ -25,52 +25,44 @@ defmodule Eventos.Service.HTTPSignatures do
Logger.debug("Signature: #{signature["signature"]}")
Logger.debug("Sigstring: #{sigstring}")
{:ok, sig} = Base.decode64(signature["signature"])
Logger.debug(inspect sig)
Logger.debug(inspect public_key)
case ExPublicKey.verify(sigstring, sig, public_key) do
{:ok, sig_valid} ->
sig_valid
{:error, err} ->
Logger.error(err)
false
end
:public_key.verify(sigstring, :sha256, sig, public_key)
end
def validate_conn(conn) do
# TODO: How to get the right key and see if it is actually valid for that request.
# For now, fetch the key for the actor.
with actor_id <- conn.params["actor"],
{:ok, public_key} <- Account.get_public_key_for_url(actor_id) do
case HTTPSign.verify(conn, public_key) do
{:ok, conn} ->
true
_ ->
Logger.debug("Could not validate, re-fetching user and trying one more time")
{:ok, public_key_code} <- Actor.get_public_key_for_url(actor_id),
[public_key] = :public_key.pem_decode(public_key_code),
public_key = :public_key.pem_entry_decode(public_key) do
if validate_conn(conn, public_key) do
true
else
Logger.info("Could not validate request, re-fetching user and trying one more time")
# Fetch user anew and try one more time
with actor_id <- conn.params["actor"],
{:ok, _user} <- ActivityPub.make_account_from_url(actor_id),
{:ok, public_key} <- Account.get_public_key_for_url(actor_id) do
case HTTPSign.verify(conn, public_key) do
{:ok, conn} ->
true
{:error, :forbidden} ->
false
end
{:ok, _actor} <- ActivityPub.make_actor_from_url(actor_id),
{:ok, public_key_code} <- Actor.get_public_key_for_url(actor_id),
[public_key] = :public_key.pem_decode(public_key_code),
public_key = :public_key.pem_entry_decode(public_key) do
validate_conn(conn, public_key)
end
end
else
e ->
Logger.debug("Could not public key!")
Logger.debug("Could not found url for actor!")
Logger.debug(inspect e)
false
end
end
# def validate_conn(conn, public_key) do
# headers = Enum.into(conn.req_headers, %{})
# signature = split_signature(headers["signature"])
# validate(headers, signature, public_key)
# end
def validate_conn(conn, public_key) do
headers = Enum.into(conn.req_headers, %{})
[host_without_port, _] = String.split(headers["host"], ":")
headers = Map.put(headers, "host", host_without_port)
signature = split_signature(headers["signature"])
validate(headers, signature, public_key)
end
def build_signing_string(headers, used_headers) do
used_headers
@@ -78,35 +70,24 @@ defmodule Eventos.Service.HTTPSignatures do
|> Enum.join("\n")
end
def sign(account, headers) do
sigstring = build_signing_string(headers, Map.keys(headers))
def sign(actor, headers) do
with {:ok, private_key_code} = Actor.get_private_key_for_actor(actor),
[private_key] = :public_key.pem_decode(private_key_code),
private_key = :public_key.pem_entry_decode(private_key) do
sigstring = build_signing_string(headers, Map.keys(headers))
{:ok, private_key} = Account.get_private_key_for_account(account)
signature =
:public_key.sign(sigstring, :sha256, private_key)
|> Base.encode64()
Logger.debug("private_key")
Logger.debug(inspect private_key)
Logger.debug("sigstring")
Logger.debug(inspect sigstring)
{:ok, signature} = HTTPSign.Crypto.sign(:rsa, sigstring, private_key)
Logger.debug(inspect signature)
signature = Base.encode64(signature)
sign = [
keyId: account.url <> "#main-key",
algorithm: "rsa-sha256",
headers: Map.keys(headers) |> Enum.join(" "),
signature: signature
]
|> Enum.map(fn {k, v} -> "#{k}=\"#{v}\"" end)
|> Enum.join(",")
Logger.debug("sign")
Logger.debug(inspect sign)
{:ok, public_key} = Account.get_public_key_for_account(account)
Logger.debug("inspect split signature inside sign")
Logger.debug(inspect split_signature(sign))
Logger.debug(inspect validate(headers, split_signature(sign), public_key))
sign
[
keyId: actor.url <> "#main-key",
algorithm: "rsa-sha256",
headers: Map.keys(headers) |> Enum.join(" "),
signature: signature
]
|> Enum.map(fn {k, v} -> "#{k}=\"#{v}\"" end)
|> Enum.join(",")
end
end
end

View File

@@ -1,6 +1,6 @@
defmodule Eventos.Service.WebFinger do
alias Eventos.Accounts
alias Eventos.Actors
alias Eventos.Service.XmlBuilder
alias Eventos.Repo
require Jason
@@ -26,14 +26,14 @@ defmodule Eventos.Service.WebFinger do
def webfinger(resource, "JSON") do
host = EventosWeb.Endpoint.host()
regex = ~r/(acct:)?(?<username>\w+)@#{host}/
regex = ~r/(acct:)?(?<name>\w+)@#{host}/
with %{"username" => username} <- Regex.named_captures(regex, resource) do
user = Accounts.get_account_by_username(username)
with %{"name" => name} <- Regex.named_captures(regex, resource) do
user = Actors.get_local_actor_by_name(name)
{:ok, represent_user(user, "JSON")}
else
_e ->
with user when not is_nil(user) <- Accounts.get_account_by_url(resource) do
with user when not is_nil(user) <- Actors.get_actor_by_url(resource) do
{:ok, represent_user(user, "JSON")}
else
_e ->
@@ -44,7 +44,7 @@ defmodule Eventos.Service.WebFinger do
def represent_user(user, "JSON") do
%{
"subject" => "acct:#{user.username}@#{EventosWeb.Endpoint.host() <> ":4001"}",
"subject" => "acct:#{user.preferred_username}@#{EventosWeb.Endpoint.host() <> ":4001"}",
"aliases" => [user.url],
"links" => [
%{"rel" => "self", "type" => "application/activity+json", "href" => user.url},
@@ -67,18 +67,18 @@ defmodule Eventos.Service.WebFinger do
{:ok, data}
end
def finger(account) do
account = String.trim_leading(account, "@")
def finger(actor) do
actor = String.trim_leading(actor, "@")
domain =
with [_name, domain] <- String.split(account, "@") do
with [_name, domain] <- String.split(actor, "@") do
domain
else
_e ->
URI.parse(account).host
URI.parse(actor).host
end
address = "http://#{domain}/.well-known/webfinger?resource=acct:#{account}"
address = "http://#{domain}/.well-known/webfinger?resource=acct:#{actor}"
with response <- HTTPoison.get(address, [Accept: "application/json"],follow_redirect: true),
{:ok, %{status_code: status_code, body: body}} when status_code in 200..299 <- response do
@@ -86,7 +86,7 @@ defmodule Eventos.Service.WebFinger do
webfinger_from_json(doc)
else
e ->
Logger.debug(fn -> "Couldn't finger #{account}" end)
Logger.debug(fn -> "Couldn't finger #{actor}" end)
Logger.debug(fn -> inspect(e) end)
{:error, e}
end