You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

107 lines
2.7 KiB

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