Merge branch '534_federation_targets_reachability' into 'develop'
authorhref <href+git-pleroma@random.sh>
Fri, 1 Feb 2019 09:14:35 +0000 (09:14 +0000)
committerhref <href+git-pleroma@random.sh>
Fri, 1 Feb 2019 09:14:35 +0000 (09:14 +0000)
[#534] Unreachable federation targets retirement

Closes #534

See merge request pleroma/pleroma!703

1  2 
config/config.exs
docs/config.md
lib/pleroma/web/activity_pub/activity_pub.ex
test/support/http_request_mock.ex
test/web/activity_pub/activity_pub_test.exs

diff --combined config/config.exs
index 98c94c14922c72c262b9de091eb7bb3e6740d9fe,8131d9b18cef08e4ec66735c245a464dbe6d1bd1..d0d53a64a2fa25127c8d9f1a226ee727b87db190
@@@ -15,20 -15,6 +15,20 @@@ config :pleroma, Pleroma.Captcha
    seconds_valid: 60,
    method: Pleroma.Captcha.Kocaptcha
  
 +config :pleroma, :hackney_pools,
 +  federation: [
 +    max_connections: 50,
 +    timeout: 150_000
 +  ],
 +  media: [
 +    max_connections: 50,
 +    timeout: 150_000
 +  ],
 +  upload: [
 +    max_connections: 25,
 +    timeout: 300_000
 +  ]
 +
  config :pleroma, Pleroma.Captcha.Kocaptcha, endpoint: "https://captcha.kotobank.ch"
  
  # Upload configuration
@@@ -36,14 -22,7 +36,14 @@@ config :pleroma, Pleroma.Upload
    uploader: Pleroma.Uploaders.Local,
    filters: [],
    proxy_remote: false,
 -  proxy_opts: []
 +  proxy_opts: [
 +    redirect_on_failure: false,
 +    max_body_length: 25 * 1_048_576,
 +    http: [
 +      follow_redirect: true,
 +      pool: :upload
 +    ]
 +  ]
  
  config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads"
  
@@@ -146,6 -125,7 +146,7 @@@ config :pleroma, :instance
    banner_upload_limit: 4_000_000,
    registrations_open: true,
    federating: true,
+   federation_reachability_timeout_days: 7,
    allow_relay: true,
    rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
    public: true,
@@@ -175,7 -155,6 +176,7 @@@ config :pleroma, :markup
      Pleroma.HTML.Scrubber.Default
    ]
  
 +# Deprecated, will be gone in 1.0
  config :pleroma, :fe,
    theme: "pleroma-dark",
    logo: "/static/logo.png",
    subject_line_behavior: "email",
    always_show_subject_input: true
  
 +config :pleroma, :frontend_configurations,
 +  pleroma_fe: %{
 +    theme: "pleroma-dark",
 +    logo: "/static/logo.png",
 +    background: "/images/city.jpg",
 +    redirectRootNoLogin: "/main/all",
 +    redirectRootLogin: "/main/friends",
 +    showInstanceSpecificPanel: true,
 +    scopeOptionsEnabled: false,
 +    formattingOptionsEnabled: false,
 +    collapseMessageWithSubject: false,
 +    hidePostStats: false,
 +    hideUserStats: false,
 +    scopeCopy: true,
 +    subjectLineBehavior: "email",
 +    alwaysShowSubjectInput: true
 +  }
 +
  config :pleroma, :activitypub,
    accept_blocks: true,
    unfollow_blocked: true,
@@@ -235,18 -196,7 +236,18 @@@ config :pleroma, :mrf_simple
    reject: [],
    accept: []
  
 -config :pleroma, :media_proxy, enabled: false
 +config :pleroma, :rich_media, enabled: true
 +
 +config :pleroma, :media_proxy,
 +  enabled: false,
 +  proxy_opts: [
 +    redirect_on_failure: false,
 +    max_body_length: 25 * 1_048_576,
 +    http: [
 +      follow_redirect: true,
 +      pool: :media
 +    ]
 +  ]
  
  config :pleroma, :chat, enabled: true
  
diff --combined docs/config.md
index 22a9b23f9258b3657d6112bcb53c9043eff88adb,26ce842d4191d0ee7bdc9320b86f964081a9ff16..c14746d932fb9c573560363d2c73ecd5769e7766
@@@ -72,6 -72,7 +72,7 @@@ config :pleroma, Pleroma.Mailer
  * `invites_enabled`: Enable user invitations for admins (depends on `registrations_open: false`).
  * `account_activation_required`: Require users to confirm their emails before signing in.
  * `federating`: Enable federation with other instances
+ * `federation_reachability_timeout_days`: Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.
  * `allow_relay`: Enable Pleroma’s Relay, which makes it possible to follow a whole instance
  * `rewrite_policy`: Message Rewrite Policy, either one or a list. Here are the ones available by default:
    * `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesn’t modify activities (default)
  * `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog
  See: [logger’s documentation](https://hexdocs.pm/logger/Logger.html) and [ex_syslogger’s documentation](https://hexdocs.pm/ex_syslogger/)
  
 +
 +## :frontend_configurations
 +
 +This can be used to configure a keyword list that keeps the configuration data for any kind of frontend. By default, settings for `pleroma_fe` are configured.
 +
 +Frontends can access these settings at `/api/pleroma/frontend_configurations`
 +
 +To add your own configuration for PleromaFE, use it like this:
 +
 +`config :pleroma, :frontend_configurations, pleroma_fe: %{redirectRootNoLogin: "/main/all", ...}`
 +
 +These settings need to be complete, they will override the defaults. See `priv/static/static/config.json` for the available keys.
 +
  ## :fe
 +__THIS IS DEPRECATED__
 +
 +If you are using this method, please change it to the `frontend_configurations` method. Please set this option to false in your config like this: `config :pleroma, :fe, false`.
 +
  This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:instance`` is set to false.
  
  * `theme`: Which theme to use, they are defined in ``styles.json``
  * `logo`: URL of the logo, defaults to Pleroma’s logo
 -* `logo_mask`: Whenether to mask the logo
 +* `logo_mask`: Whether to use only the logo's shape as a mask (true) or as a regular image (false)
  * `logo_margin`: What margin to use around the logo
  * `background`: URL of the background, unless viewing a user profile with a background that is set
  * `redirect_root_no_login`: relative URL which indicates where to redirect when a user isn’t logged in.
@@@ -234,23 -218,3 +235,23 @@@ curl "http://localhost:4000/api/pleroma
    * Pleroma.Web.Metadata.Providers.OpenGraph
    * Pleroma.Web.Metadata.Providers.TwitterCard
  * `unfurl_nsfw`: If set to `true` nsfw attachments will be shown in previews
 +
 +## :rich_media
 +* `enabled`: if enabled the instance will parse metadata from attached links to generate link previews
 +
 +## :hackney_pools
 +
 +Advanced. Tweaks Hackney (http client) connections pools.
 +
 +There's three pools used:
 +
 +* `:federation` for the federation jobs.
 +  You may want this pool max_connections to be at least equal to the number of federator jobs + retry queue jobs.
 +* `:media` for rich media, media proxy
 +* `:upload` for uploaded media (if using a remote uploader and `proxy_remote: true`)
 +
 +For each pool, the options are:
 +
 +* `max_connections` - how much connections a pool can hold
 +* `timeout` - retention duration for connections
 +
index 0199ac9e7c0558a5db0721fd2ffc71a17ff486b9,4016808e8e72b5706731173550f48279bb06dd51..06e8c3f1c28945370e547284d9cdc274cfdcd65e
@@@ -3,7 -3,7 +3,7 @@@
  # SPDX-License-Identifier: AGPL-3.0-only
  
  defmodule Pleroma.Web.ActivityPub.ActivityPub do
-   alias Pleroma.{Activity, Repo, Object, Upload, User, Notification}
+   alias Pleroma.{Activity, Repo, Object, Upload, User, Notification, Instances}
    alias Pleroma.Web.ActivityPub.{Transmogrifier, MRF}
    alias Pleroma.Web.WebFinger
    alias Pleroma.Web.Federator
            recipients: recipients
          })
  
 +      Task.start(fn ->
 +        Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
 +      end)
 +
        Notification.create_notifications(activity)
        stream_out(activity)
        {:ok, activity}
    end
  
    def publish(actor, activity) do
-     followers =
+     remote_followers =
        if actor.follower_address in activity.recipients do
          {:ok, followers} = User.get_followers(actor)
          followers |> Enum.filter(&(!&1.local))
      public = is_public?(activity)
  
      remote_inboxes =
-       (Pleroma.Web.Salmon.remote_users(activity) ++ followers)
+       (Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers)
        |> Enum.filter(fn user -> User.ap_enabled?(user) end)
        |> Enum.map(fn %{info: %{source_data: data}} ->
          (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
        end)
        |> Enum.uniq()
        |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
+       |> Instances.filter_reachable()
  
      {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
      json = Jason.encode!(data)
          digest: digest
        })
  
-     @httpoison.post(
-       inbox,
-       json,
-       [
-         {"Content-Type", "application/activity+json"},
-         {"signature", signature},
-         {"digest", digest}
-       ]
-     )
+     with {:ok, %{status: code}} when code in 200..299 <-
+            result =
+              @httpoison.post(
+                inbox,
+                json,
+                [
+                  {"Content-Type", "application/activity+json"},
+                  {"signature", signature},
+                  {"digest", digest}
+                ]
+              ) do
+       Instances.set_reachable(inbox)
+       result
+     else
+       {_post_result, response} ->
+         Instances.set_unreachable(inbox)
+         {:error, response}
+     end
    end
  
    # TODO:
index c60f618738e8c83134c77210fea5ab7506dbc39b,bcdf2e00655c81178a60b216ba74ee8197ab4e18..78e8efc9df77c1128d21bc73630e20b8752b4ec0
@@@ -143,10 -143,7 +143,10 @@@ defmodule HttpRequestMock d
       }}
    end
  
 -  def get("https://squeet.me/xrd/?uri=lain@squeet.me", _, _,
 +  def get(
 +        "https://squeet.me/xrd/?uri=lain@squeet.me",
 +        _,
 +        _,
          Accept: "application/xrd+xml,application/jrd+json"
        ) do
      {:ok,
       }}
    end
  
 -  def get("https://mst3k.interlinked.me/users/luciferMysticus", _, _,
 +  def get(
 +        "https://mst3k.interlinked.me/users/luciferMysticus",
 +        _,
 +        _,
          Accept: "application/activity+json"
        ) do
      {:ok,
       }}
    end
  
 -  def get("https://hubzilla.example.org/channel/kaniini", _, _,
 +  def get(
 +        "https://hubzilla.example.org/channel/kaniini",
 +        _,
 +        _,
          Accept: "application/activity+json"
        ) do
      {:ok,
       }}
    end
  
 -  def get("http://mastodon.example.org/@admin/99541947525187367", _, _,
 +  def get(
 +        "http://mastodon.example.org/@admin/99541947525187367",
 +        _,
 +        _,
          Accept: "application/activity+json"
        ) do
      {:ok,
       }}
    end
  
 -  def get("https://mstdn.io/users/mayuutann/statuses/99568293732299394", _, _,
 +  def get(
 +        "https://mstdn.io/users/mayuutann/statuses/99568293732299394",
 +        _,
 +        _,
          Accept: "application/activity+json"
        ) do
      {:ok,
       }}
    end
  
 -  def get("https://social.sakamoto.gq/objects/0ccc1a2c-66b0-4305-b23a-7f7f2b040056", _, _,
 +  def get(
 +        "https://social.sakamoto.gq/objects/0ccc1a2c-66b0-4305-b23a-7f7f2b040056",
 +        _,
 +        _,
          Accept: "application/atom+xml"
        ) do
      {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/httpoison_mock/sakamoto.atom")}}
       %Tesla.Env{status: 200, body: File.read!("test/fixtures/httpoison_mock/squeet.me_host_meta")}}
    end
  
 -  def get("https://squeet.me/xrd?uri=lain@squeet.me", _, _,
 +  def get(
 +        "https://squeet.me/xrd?uri=lain@squeet.me",
 +        _,
 +        _,
          Accept: "application/xrd+xml,application/jrd+json"
        ) do
      {:ok,
       }}
    end
  
 -  def get("http://framatube.org/main/xrd?uri=framasoft@framatube.org", _, _,
 +  def get(
 +        "http://framatube.org/main/xrd?uri=framasoft@framatube.org",
 +        _,
 +        _,
          Accept: "application/xrd+xml,application/jrd+json"
        ) do
      {:ok,
       }}
    end
  
 -  def get("http://gnusocial.de/main/xrd?uri=winterdienst@gnusocial.de", _, _,
 +  def get(
 +        "http://gnusocial.de/main/xrd?uri=winterdienst@gnusocial.de",
 +        _,
 +        _,
          Accept: "application/xrd+xml,application/jrd+json"
        ) do
      {:ok,
       }}
    end
  
 -  def get("https://gerzilla.de/xrd/?uri=kaniini@gerzilla.de", _, _,
 +  def get(
 +        "https://gerzilla.de/xrd/?uri=kaniini@gerzilla.de",
 +        _,
 +        _,
          Accept: "application/xrd+xml,application/jrd+json"
        ) do
      {:ok,
      {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")}}
    end
  
 +  def get("http://example.com/malformed", _, _, _) do
 +    {:ok,
 +     %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/malformed-data.html")}}
 +  end
 +
    def get("http://example.com/empty", _, _, _) do
      {:ok, %Tesla.Env{status: 200, body: "hello"}}
    end
  
+   def get("http://404.site" <> _, _, _, _) do
+     {:ok,
+      %Tesla.Env{
+        status: 404,
+        body: ""
+      }}
+   end
    def get(url, query, body, headers) do
      {:error,
       "Not implemented the mock response for get #{inspect(url)}, #{query}, #{inspect(body)}, #{
       }}
    end
  
+   def post("http://200.site" <> _, _, _, _) do
+     {:ok,
+      %Tesla.Env{
+        status: 200,
+        body: ""
+      }}
+   end
+   def post("http://connrefused.site" <> _, _, _, _) do
+     {:error, :connrefused}
+   end
+   def post("http://404.site" <> _, _, _, _) do
+     {:ok,
+      %Tesla.Env{
+        status: 404,
+        body: ""
+      }}
+   end
    def post(url, _query, _body, _headers) do
      {:error, "Not implemented the mock response for post #{inspect(url)}"}
    end
index b826f5a1baa9fd64fd2b9cd0f3a600197c224ef1,75b0918a64843c507dfd6061d8824aa65c645286..2ada4f2e5e1cd23a2bad9596b52eab027e3d9a20
@@@ -7,11 -7,12 +7,12 @@@ defmodule Pleroma.Web.ActivityPub.Activ
    alias Pleroma.Web.ActivityPub.ActivityPub
    alias Pleroma.Web.ActivityPub.Utils
    alias Pleroma.Web.CommonAPI
-   alias Pleroma.{Activity, Object, User}
+   alias Pleroma.{Activity, Object, User, Instances}
    alias Pleroma.Builders.ActivityBuilder
  
    import Pleroma.Factory
    import Tesla.Mock
+   import Mock
  
    setup do
      mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
            "in_reply_to_status_id" => private_activity_2.id
          })
  
 -      assert user1.following == [user3.ap_id <> "/followers", user1.ap_id]
 -
        activities = ActivityPub.fetch_activities([user1.ap_id | user1.following])
  
        assert [public_activity, private_activity_1, private_activity_3] == activities
      assert 3 = length(activities)
    end
  
+   describe "publish_one/1" do
+     test_with_mock "it calls `Instances.set_unreachable` on target inbox on non-2xx HTTP response code",
+                    Instances,
+                    [:passthrough],
+                    [] do
+       actor = insert(:user)
+       inbox = "http://404.site/users/nick1/inbox"
+       assert {:error, _} =
+                ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
+       assert called(Instances.set_unreachable(inbox))
+     end
+     test_with_mock "it calls `Instances.set_unreachable` on target inbox on request error of any kind",
+                    Instances,
+                    [:passthrough],
+                    [] do
+       actor = insert(:user)
+       inbox = "http://connrefused.site/users/nick1/inbox"
+       assert {:error, _} =
+                ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
+       assert called(Instances.set_unreachable(inbox))
+     end
+     test_with_mock "it does NOT call `Instances.set_unreachable` if target is reachable",
+                    Instances,
+                    [:passthrough],
+                    [] do
+       actor = insert(:user)
+       inbox = "http://200.site/users/nick1/inbox"
+       assert {:ok, _} = ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
+       refute called(Instances.set_unreachable(inbox))
+     end
+   end
    def data_uri do
      File.read!("test/fixtures/avatar_data_uri")
    end