webfinger.rb 2.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  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'].map { |link| [link['rel'], link] }.to_h
  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. res.body_with_limit
  38. elsif res.code == 404 && use_fallback
  39. body_from_host_meta
  40. elsif res.code == 410
  41. raise Webfinger::GoneError, "#{@uri} is gone from the server"
  42. else
  43. raise Webfinger::Error, "Request for #{@uri} returned HTTP #{res.code}"
  44. end
  45. end
  46. end
  47. def body_from_host_meta
  48. host_meta_request.perform do |res|
  49. if res.code == 200
  50. body_from_webfinger(url_from_template(res.body_with_limit), false)
  51. else
  52. raise Webfinger::Error, "Request for #{@uri} returned HTTP #{res.code}"
  53. end
  54. end
  55. end
  56. def url_from_template(str)
  57. link = Nokogiri::XML(str).at_xpath('//xmlns:Link[@rel="lrdd"]')
  58. if link.present?
  59. link['template'].gsub('{uri}', @uri)
  60. else
  61. raise Webfinger::Error, "Request for #{@uri} returned host-meta without link to Webfinger"
  62. end
  63. rescue Nokogiri::XML::XPath::SyntaxError
  64. raise Webfinger::Error, "Invalid XML encountered in host-meta for #{@uri}"
  65. end
  66. def host_meta_request
  67. Request.new(:get, host_meta_url).add_headers('Accept' => 'application/xrd+xml, application/xml, text/xml')
  68. end
  69. def webfinger_request(url)
  70. Request.new(:get, url).add_headers('Accept' => 'application/jrd+json, application/json')
  71. end
  72. def standard_url
  73. "https://#{@domain}/.well-known/webfinger?resource=#{@uri}"
  74. end
  75. def host_meta_url
  76. "https://#{@domain}/.well-known/host-meta"
  77. end
  78. end