about summary refs log tree commit diff
path: root/app/lib/webfinger.rb
blob: 7c0c10c3385d207df0574ed17bb8128217420873 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# frozen_string_literal: true

class Webfinger
  class Error < StandardError; end
  class GoneError < Error; end
  class RedirectError < Error; end

  class Response
    attr_reader :uri

    def initialize(uri, body)
      @uri  = uri
      @json = Oj.load(body, mode: :strict)

      validate_response!
    end

    def subject
      @json['subject']
    end

    def link(rel, attribute)
      links.dig(rel, attribute)
    end

    private

    def links
      @links ||= @json['links'].index_by { |link| link['rel'] }
    end

    def validate_response!
      raise Webfinger::Error, "Missing subject in response for #{@uri}" if subject.blank?
    end
  end

  def initialize(uri)
    _, @domain = uri.split('@')

    raise ArgumentError, 'Webfinger requested for local account' if @domain.nil?

    @uri = uri
  end

  def perform
    Response.new(@uri, body_from_webfinger)
  rescue Oj::ParseError
    raise Webfinger::Error, "Invalid JSON in response for #{@uri}"
  rescue Addressable::URI::InvalidURIError
    raise Webfinger::Error, "Invalid URI for #{@uri}"
  end

  private

  def body_from_webfinger(url = standard_url, use_fallback = true)
    webfinger_request(url).perform do |res|
      if res.code == 200
        body = res.body_with_limit
        raise Webfinger::Error, "Request for #{@uri} returned empty response" if body.empty?
        body
      elsif res.code == 404 && use_fallback
        body_from_host_meta
      elsif res.code == 410
        raise Webfinger::GoneError, "#{@uri} is gone from the server"
      else
        raise Webfinger::Error, "Request for #{@uri} returned HTTP #{res.code}"
      end
    end
  end

  def body_from_host_meta
    host_meta_request.perform do |res|
      if res.code == 200
        body_from_webfinger(url_from_template(res.body_with_limit), false)
      else
        raise Webfinger::Error, "Request for #{@uri} returned HTTP #{res.code}"
      end
    end
  end

  def url_from_template(str)
    link = Nokogiri::XML(str).at_xpath('//xmlns:Link[@rel="lrdd"]')

    if link.present?
      link['template'].gsub('{uri}', @uri)
    else
      raise Webfinger::Error, "Request for #{@uri} returned host-meta without link to Webfinger"
    end
  rescue Nokogiri::XML::XPath::SyntaxError
    raise Webfinger::Error, "Invalid XML encountered in host-meta for #{@uri}"
  end

  def host_meta_request
    Request.new(:get, host_meta_url).add_headers('Accept' => 'application/xrd+xml, application/xml, text/xml')
  end

  def webfinger_request(url)
    Request.new(:get, url).add_headers('Accept' => 'application/jrd+json, application/json')
  end

  def standard_url
    if @domain.end_with? ".onion"
      "http://#{@domain}/.well-known/webfinger?resource=#{@uri}"
    else
      "https://#{@domain}/.well-known/webfinger?resource=#{@uri}"
    end
  end

  def host_meta_url
    if @domain.end_with? ".onion"
      "http://#{@domain}/.well-known/host-meta"
    else
      "https://#{@domain}/.well-known/host-meta"
    end
  end
end