webfinger.rb 2.9 KB

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