diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index a0b7532c2e0ccedd25ba86504d635eea4ec519ea..961e82f21e24108c3c8c9f227bb5f2e01ad1ec5f 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -5,6 +5,7 @@ class ActivityPub::InboxesController < Api::BaseController include JsonLdHelper before_action :set_account + around_action :instrument_ingress def create if unknown_deleted_account? @@ -51,4 +52,10 @@ class ActivityPub::InboxesController < Api::BaseController def process_payload ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body, @account&.id) end + + def instrument_ingress + ActiveSupport::Notifications.instrument('activitypub.ingress', domain: signed_request_account.domain, ip: request.remote_ip) do + yield + end + end end diff --git a/app/controllers/api/push_controller.rb b/app/controllers/api/push_controller.rb deleted file mode 100644 index e04d19125b8d3ef67fd614e16216c5ee25539db3..0000000000000000000000000000000000000000 --- a/app/controllers/api/push_controller.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -class Api::PushController < Api::BaseController - include SignatureVerification - - def update - response, status = process_push_request - render plain: response, status: status - end - - private - - def process_push_request - case hub_mode - when 'subscribe' - Pubsubhubbub::SubscribeService.new.call(account_from_topic, hub_callback, hub_secret, hub_lease_seconds, verified_domain) - when 'unsubscribe' - Pubsubhubbub::UnsubscribeService.new.call(account_from_topic, hub_callback) - else - ["Unknown mode: #{hub_mode}", 422] - end - end - - def hub_mode - params['hub.mode'] - end - - def hub_topic - params['hub.topic'] - end - - def hub_callback - params['hub.callback'] - end - - def hub_lease_seconds - params['hub.lease_seconds'] - end - - def hub_secret - params['hub.secret'] - end - - def account_from_topic - if hub_topic.present? && local_domain? && account_feed_path? - Account.find_local(hub_topic_params[:username]) - end - end - - def hub_topic_params - @_hub_topic_params ||= Rails.application.routes.recognize_path(hub_topic_uri.path) - end - - def hub_topic_uri - @_hub_topic_uri ||= Addressable::URI.parse(hub_topic).normalize - end - - def local_domain? - TagManager.instance.web_domain?(hub_topic_domain) - end - - def verified_domain - return signed_request_account.domain if signed_request_account - end - - def hub_topic_domain - hub_topic_uri.host + (hub_topic_uri.port ? ":#{hub_topic_uri.port}" : '') - end - - def account_feed_path? - hub_topic_params[:controller] == 'accounts' && hub_topic_params[:action] == 'show' && hub_topic_params[:format] == 'atom' - end -end diff --git a/app/controllers/api/salmon_controller.rb b/app/controllers/api/salmon_controller.rb deleted file mode 100644 index ac5f3268d8c78a28428369c0166f430d71515958..0000000000000000000000000000000000000000 --- a/app/controllers/api/salmon_controller.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -class Api::SalmonController < Api::BaseController - include SignatureVerification - - before_action :set_account - respond_to :txt - - def update - if verify_payload? - process_salmon - head 202 - elsif payload.present? - render plain: signature_verification_failure_reason, status: 401 - else - head 400 - end - end - - private - - def set_account - @account = Account.find(params[:id]) - end - - def payload - @_payload ||= request.body.read - end - - def verify_payload? - payload.present? && VerifySalmonService.new.call(payload) - end - - def process_salmon - SalmonWorker.perform_async(@account.id, payload.force_encoding('UTF-8')) - end -end diff --git a/app/controllers/api/subscriptions_controller.rb b/app/controllers/api/subscriptions_controller.rb deleted file mode 100644 index 89007f3d6efbe6f26c26bb68d66a11f7e576e48d..0000000000000000000000000000000000000000 --- a/app/controllers/api/subscriptions_controller.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -class Api::SubscriptionsController < Api::BaseController - before_action :set_account - respond_to :txt - - def show - if subscription.valid?(params['hub.topic']) - @account.update(subscription_expires_at: future_expires) - render plain: encoded_challenge, status: 200 - else - head 404 - end - end - - def update - if subscription.verify(body, request.headers['HTTP_X_HUB_SIGNATURE']) - ProcessingWorker.perform_async(@account.id, body.force_encoding('UTF-8')) - end - - head 200 - end - - private - - def subscription - @_subscription ||= @account.subscription( - api_subscription_url(@account.id) - ) - end - - def body - @_body ||= request.body.read - end - - def encoded_challenge - HTMLEntities.new.encode(params['hub.challenge']) - end - - def future_expires - Time.now.utc + lease_seconds_or_default - end - - def lease_seconds_or_default - (params['hub.lease_seconds'] || 1.day).to_i.seconds - end - - def set_account - @account = Account.find(params[:id]) - end -end diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb index 9a05d96cf9cf721fc4402f227db4c7be050e66ed..f5c0e85caef6428b4e52a060f42fd22c7a3b4335 100644 --- a/app/lib/ostatus/atom_serializer.rb +++ b/app/lib/ostatus/atom_serializer.rb @@ -53,8 +53,6 @@ class OStatus::AtomSerializer append_element(feed, 'link', nil, rel: :alternate, type: 'text/html', href: ::TagManager.instance.url_for(account)) append_element(feed, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_url(account, format: 'atom')) append_element(feed, 'link', nil, rel: :next, type: 'application/atom+xml', href: account_url(account, format: 'atom', max_id: stream_entries.last.id)) if stream_entries.size == 20 - append_element(feed, 'link', nil, rel: :hub, href: api_push_url) - append_element(feed, 'link', nil, rel: :salmon, href: api_salmon_url(account.id)) stream_entries.each do |stream_entry| feed << entry(stream_entry) diff --git a/app/serializers/webfinger_serializer.rb b/app/serializers/webfinger_serializer.rb index 8c0b077020dcd2ce7833cf7063c8db3bf764bfb0..4220f697e6db66796e80eef2b984277c1c52dc64 100644 --- a/app/serializers/webfinger_serializer.rb +++ b/app/serializers/webfinger_serializer.rb @@ -18,7 +18,6 @@ class WebfingerSerializer < ActiveModel::Serializer { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(object) }, { rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(object, format: 'atom') }, { rel: 'self', type: 'application/activity+json', href: account_url(object) }, - { rel: 'salmon', href: api_salmon_url(object.id) }, { rel: 'magic-public-key', href: "data:application/magic-public-key,#{object.magic_key}" }, { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" }, ] diff --git a/app/services/subscribe_service.rb b/app/services/subscribe_service.rb index 83fd64396a1057d981ed67ca6f7c2e506ca78aac..c441e8598f9c871838d20a1f89568c11ecfe510c 100644 --- a/app/services/subscribe_service.rb +++ b/app/services/subscribe_service.rb @@ -2,57 +2,5 @@ class SubscribeService < BaseService def call(account) - return if account.hub_url.blank? - - @account = account - @account.secret = SecureRandom.hex - - build_request.perform do |response| - if response_failed_permanently? response - # We're not allowed to subscribe. Fail and move on. - @account.secret = '' - @account.save! - elsif response_successful? response - # The subscription will be confirmed asynchronously. - @account.save! - else - # The response was either a 429 rate limit, or a 5xx error. - # We need to retry at a later time. Fail loudly! - raise Mastodon::UnexpectedResponseError, response - end - end - end - - private - - def build_request - request = Request.new(:post, @account.hub_url, form: subscription_params) - request.on_behalf_of(some_local_account) if some_local_account - request - end - - def subscription_params - { - 'hub.topic': @account.remote_url, - 'hub.mode': 'subscribe', - 'hub.callback': api_subscription_url(@account.id), - 'hub.verify': 'async', - 'hub.secret': @account.secret, - 'hub.lease_seconds': 7.days.seconds, - } - end - - def some_local_account - @some_local_account ||= Account.local.without_suspended.first - end - - # Any response in the 3xx or 4xx range, except for 429 (rate limit) - def response_failed_permanently?(response) - (response.status.redirect? || response.status.client_error?) && !response.status.too_many_requests? - end - - # Any response in the 2xx range - def response_successful?(response) - response.status.success? end end diff --git a/app/services/unsubscribe_service.rb b/app/services/unsubscribe_service.rb index 95c1fb4fc01acf28e241d1cd6903734d880880c3..5fc40e63c6d48175b4cfd919029f59f81b3bedd2 100644 --- a/app/services/unsubscribe_service.rb +++ b/app/services/unsubscribe_service.rb @@ -2,35 +2,5 @@ class UnsubscribeService < BaseService def call(account) - return if account.hub_url.blank? - - @account = account - - begin - build_request.perform do |response| - Rails.logger.debug "PuSH unsubscribe for #{@account.acct} failed: #{response.status}" unless response.status.success? - end - rescue HTTP::Error, OpenSSL::SSL::SSLError => e - Rails.logger.debug "PuSH unsubscribe for #{@account.acct} failed: #{e}" - end - - @account.secret = '' - @account.subscription_expires_at = nil - @account.save! - end - - private - - def build_request - Request.new(:post, @account.hub_url, form: subscription_params) - end - - def subscription_params - { - 'hub.topic': @account.remote_url, - 'hub.mode': 'unsubscribe', - 'hub.callback': api_subscription_url(@account.id), - 'hub.verify': 'async', - } end end diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index 950e618477b8b006456da96f472ae2cbb501e9f3..de7d2a8ba3004ad8c17cf0efc2c40dd519ac6abc 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -7,7 +7,6 @@ - if @account.user&.setting_noindex %meta{ name: 'robots', content: 'noindex' }/ - %link{ rel: 'salmon', href: api_salmon_url(@account.id) }/ %link{ rel: 'alternate', type: 'application/atom+xml', href: account_url(@account, format: 'atom') }/ %link{ rel: 'alternate', type: 'application/rss+xml', href: account_url(@account, format: 'rss') }/ %link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@account) }/ diff --git a/app/views/well_known/webfinger/show.xml.ruby b/app/views/well_known/webfinger/show.xml.ruby index 968c8c1380c0d70543e60ce8f9348e18a5f1735d..c82cdb7b3d017420ac32a110e5e349296fc20e3b 100644 --- a/app/views/well_known/webfinger/show.xml.ruby +++ b/app/views/well_known/webfinger/show.xml.ruby @@ -25,11 +25,6 @@ doc << Ox::Element.new('XRD').tap do |xrd| link['href'] = account_url(@account) end - xrd << Ox::Element.new('Link').tap do |link| - link['rel'] = 'salmon' - link['href'] = api_salmon_url(@account.id) - end - xrd << Ox::Element.new('Link').tap do |link| link['rel'] = 'magic-public-key' link['href'] = "data:application/magic-public-key,#{@account.magic_key}" diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb index 5e4c391f0d79977ad2d9fb0887f2515345ef0692..bc081f7b96dd4ee323c9b1378630119ceaebd44c 100644 --- a/app/workers/activitypub/delivery_worker.rb +++ b/app/workers/activitypub/delivery_worker.rb @@ -18,7 +18,9 @@ class ActivityPub::DeliveryWorker @source_account = Account.find(source_account_id) @inbox_url = inbox_url - perform_request + ActiveSupport::Notifications.instrument('activitypub.egress', domain: Addressable::URI.parse(@inbox_url).normalized_host) do + perform_request + end failure_tracker.track_success! rescue => e diff --git a/app/workers/pubsubhubbub/delivery_worker.rb b/app/workers/pubsubhubbub/delivery_worker.rb index 619bfa48aad215c36f357114e608c62d8cbee336..077a8d676100ce43d7c54186a9cd3ede084dbf5a 100644 --- a/app/workers/pubsubhubbub/delivery_worker.rb +++ b/app/workers/pubsubhubbub/delivery_worker.rb @@ -52,11 +52,7 @@ class Pubsubhubbub::DeliveryWorker end def link_header - LinkHeader.new([hub_link_header, self_link_header]).to_s - end - - def hub_link_header - [api_push_url, [%w(rel hub)]] + LinkHeader.new([self_link_header]).to_s end def self_link_header diff --git a/config/initializers/instrumentation.rb b/config/initializers/instrumentation.rb index 8483f2be2e4caf502fe10dc12298eb32b514442d..476ddc7a015e294a3235f721de9294af92865eef 100644 --- a/config/initializers/instrumentation.rb +++ b/config/initializers/instrumentation.rb @@ -16,3 +16,39 @@ ActiveSupport::Notifications.subscribe(/process_action.action_controller/) do |* ActiveSupport::Notifications.instrument :performance, action: :measure, measurement: "#{key}.view_time", value: event.payload[:view_runtime] ActiveSupport::Notifications.instrument :performance, measurement: "#{key}.status.#{status}" end + +EXPIRE_AFTER = 2.days.seconds.freeze + +ActiveSupport::Notifications.subscribe(/activitypub.(ingress|egress)/) do |*args| + event = ActiveSupport::Notifications::Event.new(*args) + buckets = [event.started.to_i % 3_600, event.started.to_i % 86_400] + + case event.name + when 'activitypub.ingress' + buckets.each do |bucket| + Redis.current.hincrby("counters:activitypub.ingress:#{bucket}", "domain:#{event.payload[:domain]}", 1) + Redis.current.hincrby("counters:activitypub.ingress:#{bucket}", "ip:#{event.payload[:ip]}", 1) + Redis.current.expire("counters:activitypub.ingress:#{bucket}", EXPIRE_AFTER) + end + when 'activitypub.egress' + buckets.each do |bucket| + Redis.current.hincrby("counters:activitypub.egress:#{bucket}", "domain:#{event.payload[:domain]}", 1) + Redis.current.expire("counters:activitypub.egress:#{bucket}", EXPIRE_AFTER) + end + end +end + +def anomalies + now = Time.now.to_i + + [3_600, 86_400].each do |interval| + current_period = Redis.current.hgetall("counters:activitypub.ingress:#{now % interval}") + past_period = Redis.current.hgetall("counters:activitypub.ingress:#{now % interval - interval}") + + current_period.each_pair do |key, value| + if value > past_period[key] + # Sound the alarm! + end + end + end +end diff --git a/config/navigation.rb b/config/navigation.rb index df10241892997bfde20f25ac999d5e8ac7b8ae0a..ef845d1fc5957a5bd03e6fb6eb40e87a9f620c2f 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -48,7 +48,6 @@ SimpleNavigation::Configuration.run do |navigation| s.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), edit_admin_settings_url, if: -> { current_user.admin? }, highlights_on: %r{/admin/settings} s.item :custom_emojis, safe_join([fa_icon('smile-o fw'), t('admin.custom_emojis.title')]), admin_custom_emojis_url, highlights_on: %r{/admin/custom_emojis} s.item :relays, safe_join([fa_icon('exchange fw'), t('admin.relays.title')]), admin_relays_url, if: -> { current_user.admin? }, highlights_on: %r{/admin/relays} - s.item :subscriptions, safe_join([fa_icon('paper-plane-o fw'), t('admin.subscriptions.title')]), admin_subscriptions_url, if: -> { current_user.admin? } s.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url, link_html: { target: 'sidekiq' }, if: -> { current_user.admin? } s.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url, link_html: { target: 'pghero' }, if: -> { current_user.admin? } end diff --git a/config/routes.rb b/config/routes.rb index 764db8db2e61882d1dc2507de5a50fa04beb2f58..e6bc57a3158a1b2c63c3fb8c5b1c115452df7cad 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -154,7 +154,6 @@ Rails.application.routes.draw do namespace :admin do get '/dashboard', to: 'dashboard#index' - resources :subscriptions, only: [:index] resources :domain_blocks, only: [:new, :create, :show, :destroy] resources :email_domain_blocks, only: [:index, :new, :create, :destroy] resources :action_logs, only: [:index] @@ -257,16 +256,6 @@ Rails.application.routes.draw do get '/admin', to: redirect('/admin/dashboard', status: 302) namespace :api do - # PubSubHubbub outgoing subscriptions - resources :subscriptions, only: [:show] - post '/subscriptions/:id', to: 'subscriptions#update' - - # PubSubHubbub incoming subscriptions - post '/push', to: 'push#update', as: :push - - # Salmon - post '/salmon/:id', to: 'salmon#update', as: :salmon - # OEmbed get '/oembed', to: 'oembed#show', as: :oembed