fetcher.ex 5.47 KB
Newer Older
1 2 3 4
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

5
defmodule Pleroma.Object.Fetcher do
William Pitcock's avatar
William Pitcock committed
6
  alias Pleroma.HTTP
7
  alias Pleroma.Object
8
  alias Pleroma.Object.Containment
rinpatch's avatar
rinpatch committed
9
  alias Pleroma.Repo
10 11
  alias Pleroma.Signature
  alias Pleroma.Web.ActivityPub.InternalFetchActor
12 13 14
  alias Pleroma.Web.ActivityPub.Transmogrifier

  require Logger
15
  require Pleroma.Constants
16

17 18 19 20 21 22 23 24
  defp touch_changeset(changeset) do
    updated_at =
      NaiveDateTime.utc_now()
      |> NaiveDateTime.truncate(:second)

    Ecto.Changeset.put_change(changeset, :updated_at, updated_at)
  end

25 26 27 28 29 30 31 32
  defp maybe_reinject_internal_fields(data, %{data: %{} = old_data}) do
    internal_fields = Map.take(old_data, Pleroma.Constants.object_internal_fields())

    Map.merge(data, internal_fields)
  end

  defp maybe_reinject_internal_fields(data, _), do: data

33
  @spec reinject_object(struct(), map()) :: {:ok, Object.t()} | {:error, any()}
rinpatch's avatar
rinpatch committed
34
  defp reinject_object(struct, data) do
35 36 37
    Logger.debug("Reinjecting object #{data["id"]}")

    with data <- Transmogrifier.fix_object(data),
38
         data <- maybe_reinject_internal_fields(data, struct),
rinpatch's avatar
rinpatch committed
39
         changeset <- Object.change(struct, %{data: data}),
40
         changeset <- touch_changeset(changeset),
41 42
         {:ok, object} <- Repo.insert_or_update(changeset),
         {:ok, object} <- Object.set_cache(object) do
43 44 45 46 47 48 49 50
      {:ok, object}
    else
      e ->
        Logger.error("Error while processing object: #{inspect(e)}")
        {:error, e}
    end
  end

rinpatch's avatar
rinpatch committed
51
  def refetch_object(%Object{data: %{"id" => id}} = object) do
rinpatch's avatar
rinpatch committed
52 53
    with {:local, false} <- {:local, String.starts_with?(id, Pleroma.Web.base_url() <> "/")},
         {:ok, data} <- fetch_and_contain_remote_object_from_id(id),
rinpatch's avatar
rinpatch committed
54 55 56
         {:ok, object} <- reinject_object(object, data) do
      {:ok, object}
    else
57
      {:local, true} -> {:ok, object}
rinpatch's avatar
rinpatch committed
58 59 60 61
      e -> {:error, e}
    end
  end

62 63
  # TODO:
  # This will create a Create activity, which we need internally at the moment.
64
  def fetch_object_from_id(id, options \\ []) do
Maksim's avatar
Maksim committed
65 66 67 68 69
    with {:fetch_object, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
         {:fetch, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
         {:normalize, nil} <- {:normalize, Object.normalize(data, false)},
         params <- prepare_activity_params(data),
         {:containment, :ok} <- {:containment, Containment.contain_origin(id, params)},
70 71
         {:transmogrifier, {:ok, activity}} <-
           {:transmogrifier, Transmogrifier.handle_incoming(params, options)},
Maksim's avatar
Maksim committed
72 73
         {:object, _data, %Object{} = object} <-
           {:object, data, Object.normalize(activity, false)} do
74 75
      {:ok, object}
    else
Maksim's avatar
Maksim committed
76 77
      {:containment, _} ->
        {:error, "Object containment failed."}
78

79
      {:transmogrifier, {:error, {:reject, nil}}} ->
Maksim's avatar
Maksim committed
80
        {:reject, nil}
81

82 83 84
      {:transmogrifier, _} ->
        {:error, "Transmogrifier failure."}

Maksim's avatar
Maksim committed
85
      {:object, data, nil} ->
86
        reinject_object(%Object{}, data)
87

Maksim's avatar
Maksim committed
88 89
      {:normalize, object = %Object{}} ->
        {:ok, object}
90

Maksim's avatar
Maksim committed
91 92
      {:fetch_object, %Object{} = object} ->
        {:ok, object}
93

Steven Fuchs's avatar
Steven Fuchs committed
94 95 96
      {:fetch, {:error, error}} ->
        {:error, error}

97 98
      e ->
        e
99
    end
Maksim's avatar
Maksim committed
100 101 102 103 104 105 106 107 108 109 110
  end

  defp prepare_activity_params(data) do
    %{
      "type" => "Create",
      "to" => data["to"],
      "cc" => data["cc"],
      # Should we seriously keep this attributedTo thing?
      "actor" => data["actor"] || data["attributedTo"],
      "object" => data
    }
111 112
  end

113 114
  def fetch_object_from_id!(id, options \\ []) do
    with {:ok, object} <- fetch_object_from_id(id, options) do
115 116
      object
    else
Steven Fuchs's avatar
Steven Fuchs committed
117 118 119
      {:error, %Tesla.Mock.Error{}} ->
        nil

120 121
      e ->
        Logger.error("Error while fetching #{id}: #{inspect(e)}")
122 123 124 125
        nil
    end
  end

126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
  defp make_signature(id, date) do
    uri = URI.parse(id)

    signature =
      InternalFetchActor.get_actor()
      |> Signature.sign(%{
        "(request-target)": "get #{uri.path}",
        host: uri.host,
        date: date
      })

    [{:Signature, signature}]
  end

  defp sign_fetch(headers, id, date) do
    if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do
      headers ++ make_signature(id, date)
    else
      headers
    end
  end

  defp maybe_date_fetch(headers, date) do
    if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do
      headers ++ [{:Date, date}]
    else
      headers
    end
  end

156
  def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
157
    Logger.info("Fetching object #{id} via AP")
158

Maksim's avatar
Maksim committed
159
    date = Pleroma.Signature.signed_date()
160 161 162 163 164 165 166 167

    headers =
      [{:Accept, "application/activity+json"}]
      |> maybe_date_fetch(date)
      |> sign_fetch(id, date)

    Logger.debug("Fetch headers: #{inspect(headers)}")

168
    with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")},
169
         {:ok, %{body: body, status: code}} when code in 200..299 <- HTTP.get(id, headers),
170 171 172 173
         {:ok, data} <- Jason.decode(body),
         :ok <- Containment.contain_origin_from_id(id, data) do
      {:ok, data}
    else
minibikini's avatar
minibikini committed
174
      {:ok, %{status: code}} when code in [404, 410] ->
minibikini's avatar
minibikini committed
175 176
        {:error, "Object has been deleted"}

177 178 179
      {:scheme, _} ->
        {:error, "Unsupported URI scheme"}

Steven Fuchs's avatar
Steven Fuchs committed
180 181 182
      {:error, e} ->
        {:error, e}

183 184
      e ->
        {:error, e}
185 186
    end
  end
187

rinpatch's avatar
rinpatch committed
188 189 190
  def fetch_and_contain_remote_object_from_id(%{"id" => id}),
    do: fetch_and_contain_remote_object_from_id(id)

191
  def fetch_and_contain_remote_object_from_id(_id), do: {:error, "id must be a string"}
192
end