116 lines
		
	
	
	
		
			2.9 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			116 lines
		
	
	
	
		
			2.9 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
| # frozen_string_literal: true
 | |
| 
 | |
| class Webfinger
 | |
|   class Error < StandardError; end
 | |
|   class GoneError < Error; end
 | |
|   class RedirectError < StandardError; 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
 |