webfinger.rb 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. # frozen_string_literal: true
  2. class Webfinger
  3. class Error < StandardError; end
  4. class GoneError < Error; end
  5. class RedirectError < Error; end
  6. class Response
  7. ACTIVITYPUB_READY_TYPE = ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].freeze
  8. attr_reader :uri
  9. def initialize(uri, body)
  10. @uri = uri
  11. @json = Oj.load(body, mode: :strict)
  12. validate_response!
  13. end
  14. def subject
  15. @json['subject']
  16. end
  17. def link(rel, attribute)
  18. links.dig(rel, 0, attribute)
  19. end
  20. def self_link_href
  21. self_link.fetch('href')
  22. end
  23. private
  24. def links
  25. @links ||= @json.fetch('links', []).group_by { |link| link['rel'] }
  26. end
  27. def self_link
  28. links.fetch('self', []).find do |link|
  29. ACTIVITYPUB_READY_TYPE.include?(link['type'])
  30. end
  31. end
  32. def validate_response!
  33. raise Webfinger::Error, "Missing subject in response for #{@uri}" if subject.blank?
  34. raise Webfinger::Error, "Missing self link in response for #{@uri}" if self_link.blank?
  35. end
  36. end
  37. def initialize(uri)
  38. _, @domain = uri.split('@')
  39. raise ArgumentError, 'Webfinger requested for local account' if @domain.nil?
  40. @uri = uri
  41. end
  42. def perform
  43. Response.new(@uri, body_from_webfinger)
  44. rescue Oj::ParseError
  45. raise Webfinger::Error, "Invalid JSON in response for #{@uri}"
  46. rescue Addressable::URI::InvalidURIError
  47. raise Webfinger::Error, "Invalid URI for #{@uri}"
  48. end
  49. private
  50. def body_from_webfinger(url = standard_url, use_fallback = true)
  51. webfinger_request(url).perform do |res|
  52. if res.code == 200
  53. body = res.body_with_limit
  54. raise Webfinger::Error, "Request for #{@uri} returned empty response" if body.empty?
  55. body
  56. elsif res.code == 404 && use_fallback
  57. body_from_host_meta
  58. elsif res.code == 410
  59. raise Webfinger::GoneError, "#{@uri} is gone from the server"
  60. else
  61. raise Webfinger::Error, "Request for #{@uri} returned HTTP #{res.code}"
  62. end
  63. end
  64. end
  65. def body_from_host_meta
  66. host_meta_request.perform do |res|
  67. if res.code == 200
  68. body_from_webfinger(url_from_template(res.body_with_limit), false)
  69. else
  70. raise Webfinger::Error, "Request for #{@uri} returned HTTP #{res.code}"
  71. end
  72. end
  73. end
  74. def url_from_template(str)
  75. link = Nokogiri::XML(str).at_xpath('//xmlns:Link[@rel="lrdd"]')
  76. if link.present?
  77. link['template'].gsub('{uri}', @uri)
  78. else
  79. raise Webfinger::Error, "Request for #{@uri} returned host-meta without link to Webfinger"
  80. end
  81. rescue Nokogiri::XML::XPath::SyntaxError
  82. raise Webfinger::Error, "Invalid XML encountered in host-meta for #{@uri}"
  83. end
  84. def host_meta_request
  85. Request.new(:get, host_meta_url).add_headers('Accept' => 'application/xrd+xml, application/xml, text/xml')
  86. end
  87. def webfinger_request(url)
  88. Request.new(:get, url).add_headers('Accept' => 'application/jrd+json, application/json')
  89. end
  90. def standard_url
  91. if @domain.end_with? '.onion'
  92. "http://#{@domain}/.well-known/webfinger?resource=#{@uri}"
  93. else
  94. "https://#{@domain}/.well-known/webfinger?resource=#{@uri}"
  95. end
  96. end
  97. def host_meta_url
  98. if @domain.end_with? '.onion'
  99. "http://#{@domain}/.well-known/host-meta"
  100. else
  101. "https://#{@domain}/.well-known/host-meta"
  102. end
  103. end
  104. end