-
-
Notifications
You must be signed in to change notification settings - Fork 934
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Verify links in version metadata & homepage (#4062)
* Verify links in version metadata & homepage So they can be displayed distinctively in gem (& eventually user) pages. * Use beginning of day in verification queries Using a constant time makes the queries cachable --------- Co-authored-by: Arlette Thibodeau <[email protected]>
- Loading branch information
1 parent
8907e31
commit c3c61e1
Showing
22 changed files
with
714 additions
and
50 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
class LinkVerificationResource < Avo::BaseResource | ||
self.title = :id | ||
self.includes = [] | ||
# self.search_query = -> do | ||
# scope.ransack(id_eq: params[:q], m: "or").result(distinct: false) | ||
# end | ||
|
||
field :id, as: :id | ||
# Fields generated from the model | ||
field :linkable, as: :belongs_to, | ||
polymorphic_as: :linkable, | ||
types: [::Rubygem] | ||
field :uri, as: :text | ||
field :verified?, as: :boolean | ||
field :last_verified_at, as: :date_time | ||
field :last_failure_at, as: :date_time | ||
field :failures_since_last_verification, as: :number | ||
# add fields here | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# This controller has been generated to enable Rails' resource routes. | ||
# More information on https://docs.avohq.io/2.0/controllers.html | ||
class Avo::LinkVerificationsController < Avo::ResourcesController | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
class VerifyLinkJob < ApplicationJob | ||
queue_as :default | ||
|
||
retry_on ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid, wait: :exponentially_longer, attempts: 3 | ||
|
||
ERRORS = (HTTP_ERRORS + [Faraday::Error, SocketError, SystemCallError, OpenSSL::SSL::SSLError]).freeze | ||
|
||
class NotHTTPSError < StandardError; end | ||
class LinkNotPresentError < StandardError; end | ||
class HTTPResponseError < StandardError; end | ||
|
||
discard_on NotHTTPSError do |job, _error| | ||
job.record_failure | ||
end | ||
|
||
rescue_from LinkNotPresentError, HTTPResponseError, *ERRORS do |error| | ||
logger.info "Linkback verification failed with error: #{error.message}", error: error, uri: link_verification.uri, | ||
linkable: link_verification.linkable | ||
|
||
link_verification.transaction do | ||
record_failure | ||
if should_retry? | ||
retry_job(wait: 5.seconds * (3.5**link_verification.failures_since_last_verification.pred), error:) | ||
else | ||
instrument :retry_stopped, error: error | ||
end | ||
end | ||
end | ||
|
||
TIMEOUT_SEC = 5 | ||
|
||
def perform(link_verification:) | ||
verify_link!(link_verification.uri, link_verification.linkable) | ||
record_success | ||
end | ||
|
||
def link_verification | ||
arguments.first.fetch(:link_verification) | ||
end | ||
|
||
def verify_link!(uri, linkable) | ||
raise NotHTTPSError unless uri.start_with?("https://") | ||
|
||
expected_href = linkable.linkable_verification_uri.to_s.downcase | ||
|
||
response = get(uri) | ||
raise HTTPResponseError, "Expected 200, got #{response.status}" unless response.status == 200 | ||
# TODO: body_with_limit, https://github.com/mastodon/mastodon/blob/33c8708a1ac7df363bf2bd74ab8fa2ed7168379c/app/lib/request.rb#L246 | ||
doc = Nokogiri::HTML5(response.body) | ||
|
||
xpaths = [ | ||
# rel=me, what mastodon uses for profile link verification | ||
'//a[contains(concat(" ", normalize-space(@rel), " "), " me ")]', | ||
'//link[contains(concat(" ", normalize-space(@rel), " "), " me ")]', | ||
|
||
# rel=rubygem | ||
'//a[contains(concat(" ", normalize-space(@rel), " "), " rubygem ")]', | ||
'//link[contains(concat(" ", normalize-space(@rel), " "), " rubygem ")]' | ||
] | ||
|
||
if URI(uri).host == "github.com" | ||
# github doesn't set a rel attribute on the URL added to a repo, so we have to use role=link instead | ||
xpaths << '//a[contains(concat(" ", normalize-space(@role), " "), " link ")]' | ||
xpaths << '//link[contains(concat(" ", normalize-space(@role), " "), " link ")]' | ||
end | ||
|
||
links = doc.xpath(xpaths.join("|")) | ||
|
||
return if links.any? { |link| link["href"]&.downcase == expected_href } | ||
raise LinkNotPresentError, "Expected #{expected_href} to be present in #{uri}" | ||
end | ||
|
||
def get(url) | ||
Faraday.new(nil, request: { timeout: TIMEOUT_SEC }) do |f| | ||
f.response :logger, logger, headers: false, errors: true | ||
f.response :raise_error | ||
end.get( | ||
url, | ||
{}, | ||
{ | ||
"User-Agent" => "RubyGems.org Linkback Verification/#{AppRevision.version}", | ||
"Accept" => "text/html" | ||
} | ||
) | ||
end | ||
|
||
def should_retry? | ||
link_verification.failures_since_last_verification < LinkVerification::MAX_FAILURES | ||
end | ||
|
||
def record_success | ||
link_verification.update!( | ||
last_verified_at: Time.current, | ||
failures_since_last_verification: 0 | ||
) | ||
end | ||
|
||
def record_failure | ||
link_verification.touch(:last_failure_at) | ||
link_verification.increment!(:failures_since_last_verification) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
class LinkVerification < ApplicationRecord | ||
belongs_to :linkable, polymorphic: true | ||
|
||
MAX_FAILURES = 10 | ||
VALIDITY = 1.month | ||
|
||
def self.verified | ||
where(last_verified_at: VALIDITY.ago.beginning_of_day..) | ||
end | ||
|
||
def self.unverified | ||
never_verified | ||
.or(last_verified_before(VALIDITY.ago.beginning_of_day)) | ||
end | ||
|
||
def self.never_verified | ||
where(last_verified_at: nil) | ||
end | ||
|
||
def self.last_verified_before(time) | ||
where(last_verified_at: ...time) | ||
end | ||
|
||
def self.pending_verification | ||
never_verified | ||
.or(last_verified_before(3.weeks.ago.beginning_of_day)) | ||
.where(failures_since_last_verification: 0) | ||
.https_uri | ||
end | ||
|
||
def self.https_uri | ||
where(arel_table[:uri].matches("https://%")) | ||
end | ||
|
||
def self.linkable(linkable) | ||
where(linkable:) | ||
end | ||
|
||
def self.for_uri(uri) | ||
where(uri:) | ||
end | ||
|
||
def unverified? | ||
!verified? | ||
end | ||
|
||
def verified? | ||
return false unless (verified_at = last_verified_at.presence) | ||
|
||
verified_at > VALIDITY.ago | ||
end | ||
|
||
def should_verify? | ||
return false unless https? | ||
return false unless failures_since_last_verification <= 0 | ||
|
||
unverified? || last_verified_at.before?(3.weeks.ago.beginning_of_day) | ||
end | ||
|
||
def verify_later | ||
VerifyLinkJob.perform_later(link_verification: self) | ||
end | ||
|
||
def retry_if_needed | ||
if previously_new_record? && should_verify? | ||
verify_later | ||
return self | ||
end | ||
|
||
return unless https? | ||
return unless failures_since_last_verification.positive? && last_failure_at.present? | ||
return unless last_verified_at.nil? || last_verified_at.before?(last_failure_at) | ||
|
||
update!(failures_since_last_verification: 0) | ||
end | ||
|
||
def https? | ||
uri.start_with?("https://") | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
class LinkVerificationPolicy < ApplicationPolicy | ||
class Scope < Scope | ||
def resolve | ||
scope.all | ||
end | ||
end | ||
|
||
def avo_index? = rubygems_org_admin? | ||
def avo_show? = rubygems_org_admin? | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
class CreateLinkVerifications < ActiveRecord::Migration[7.0] | ||
def change | ||
create_table :link_verifications do |t| | ||
t.references :linkable, polymorphic: true, null: false | ||
t.string :uri, null: false | ||
t.datetime :last_verified_at, null: true | ||
t.datetime :last_failure_at, null: true | ||
t.integer :failures_since_last_verification, default: 0 | ||
|
||
t.timestamps | ||
|
||
t.index ["linkable_id", "linkable_type", "uri"], name: "index_link_verifications_on_linkable_and_uri" | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.