├── .ruby-version ├── .rspec ├── .gitignore ├── Rakefile ├── .travis.yml ├── Gemfile ├── lib ├── goldfinger │ ├── utils.rb │ ├── request.rb │ ├── client.rb │ ├── link.rb │ └── result.rb └── goldfinger.rb ├── spec ├── goldfinger │ ├── request_spec.rb │ ├── client_spec.rb │ └── result_spec.rb ├── spec_helper.rb └── fixtures │ ├── quitter.no_.well-known_host-meta │ ├── quitter.no_.well-known_webfinger.xml │ └── quitter.no_.well-known_webfinger.json ├── goldfinger.gemspec ├── LICENSE ├── .github └── workflows │ └── gempush.yml ├── README.md ├── .rubocop.yml └── Gemfile.lock /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.5.0 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | --format Fuubar 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.gem 11 | 12 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | require 'bundler/gem_tasks' 3 | 4 | RSpec::Core::RakeTask.new(:spec) do |task| 5 | task.rspec_opts = ['--color', '--format', 'documentation'] 6 | end 7 | 8 | task :default => :spec 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | cache: bundler 3 | 4 | notifications: 5 | email: false 6 | 7 | rvm: 8 | - 2.3.1 9 | - 2.4.1 10 | 11 | bundler_args: --without development --retry=3 --jobs=3 12 | 13 | script: bundle exec rake 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | group :development, :test do 4 | gem 'pry', '>= 0.10.3' 5 | end 6 | 7 | group :test do 8 | gem 'rspec', '>= 3.0' 9 | gem 'fuubar' 10 | gem 'webmock' 11 | gem 'rake' 12 | end 13 | 14 | gemspec 15 | -------------------------------------------------------------------------------- /lib/goldfinger/utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Goldfinger 4 | module Utils 5 | def perform_get(path, options = {}) 6 | perform_request(:get, path, options) 7 | end 8 | 9 | def perform_request(request_method, path, options = {}) 10 | Goldfinger::Request.new(request_method, path, options).perform 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/goldfinger/request_spec.rb: -------------------------------------------------------------------------------- 1 | describe Goldfinger::Request do 2 | describe '#perform' do 3 | before do 4 | stub_request(:get, 'example.com').to_return(body: 'OK') 5 | end 6 | 7 | subject { Goldfinger::Request.new(:get, 'http://example.com').perform } 8 | 9 | it 'returns a http response' do 10 | expect(subject).to be_a HTTP::Response 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'goldfinger' 2 | require 'webmock/rspec' 3 | require 'pry' 4 | 5 | WebMock.disable_net_connect! 6 | 7 | RSpec.configure do |config| 8 | config.expect_with :rspec do |expectations| 9 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 10 | end 11 | 12 | config.mock_with :rspec do |mocks| 13 | mocks.verify_partial_doubles = true 14 | end 15 | end 16 | 17 | def fixture_path(path) 18 | File.join(File.expand_path('../fixtures', __FILE__), path) 19 | end 20 | 21 | def fixture(path) 22 | File.new(fixture_path(path)) 23 | end 24 | -------------------------------------------------------------------------------- /lib/goldfinger/request.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'http' 4 | require 'addressable' 5 | 6 | module Goldfinger 7 | class Request 8 | def initialize(request_method, path, options = {}) 9 | @request_method = request_method 10 | @uri = Addressable::URI.parse(path) 11 | @options = options 12 | end 13 | 14 | def perform 15 | http_client.request(@request_method, @uri.to_s, @options) 16 | end 17 | 18 | private 19 | 20 | def http_client 21 | HTTP.timeout(write: 60, connect: 20, read: 60).follow 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/fixtures/quitter.no_.well-known_host-meta: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /lib/goldfinger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'goldfinger/request' 4 | require 'goldfinger/link' 5 | require 'goldfinger/result' 6 | require 'goldfinger/utils' 7 | require 'goldfinger/client' 8 | 9 | module Goldfinger 10 | class Error < StandardError 11 | end 12 | 13 | class NotFoundError < Error 14 | end 15 | 16 | # Returns result for the Webfinger query 17 | # 18 | # @raise [Goldfinger::NotFoundError] Error raised when the Webfinger resource could not be retrieved 19 | # @raise [Goldfinger::SSLError] Error raised when there was a SSL error when fetching the resource 20 | # @param uri [String] A full resource identifier in the format acct:user@example.com 21 | # @param opts [Hash] Options passed to HTTP.rb client 22 | # @return [Goldfinger::Result] 23 | def self.finger(uri, opts = {}) 24 | Goldfinger::Client.new(uri, opts).finger 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /goldfinger.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Gem::Specification.new do |s| 4 | s.name = 'goldfinger' 5 | s.version = '2.1.1' 6 | s.platform = Gem::Platform::RUBY 7 | s.required_ruby_version = '>= 2.3.0' 8 | s.date = '2016-02-17' 9 | s.summary = 'A Webfinger utility for Ruby' 10 | s.description = 'A Webfinger utility for Ruby' 11 | s.authors = ['Eugen Rochko'] 12 | s.email = 'eugen@zeonfederated.com' 13 | s.files = `git ls-files lib LICENSE README.md`.split($RS) 14 | s.homepage = 'https://github.com/tootsuite/goldfinger' 15 | s.licenses = ['MIT'] 16 | 17 | s.add_dependency('http', '~> 4.0') 18 | s.add_dependency('addressable', '~> 2.5') 19 | s.add_dependency('nokogiri', '~> 1.8') 20 | s.add_dependency('oj', '~> 3.0') 21 | 22 | s.add_development_dependency('bundler', '~> 1.15') 23 | end 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Eugen Rochko 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /.github/workflows/gempush.yml: -------------------------------------------------------------------------------- 1 | name: Ruby Gem 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | build: 10 | name: Build + Publish 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@master 15 | - name: Set up Ruby 2.6 16 | uses: actions/setup-ruby@v1 17 | with: 18 | version: 2.6.x 19 | 20 | - name: Publish to GPR 21 | run: | 22 | mkdir -p $HOME/.gem 23 | touch $HOME/.gem/credentials 24 | chmod 0600 $HOME/.gem/credentials 25 | printf -- "---\n:github: Bearer ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials 26 | gem build *.gemspec 27 | gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem 28 | env: 29 | GEM_HOST_API_KEY: ${{secrets.GPR_AUTH_TOKEN}} 30 | OWNER: tootsuite 31 | 32 | - name: Publish to RubyGems 33 | run: | 34 | mkdir -p $HOME/.gem 35 | touch $HOME/.gem/credentials 36 | chmod 0600 $HOME/.gem/credentials 37 | printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials 38 | gem build *.gemspec 39 | gem push *.gem 40 | env: 41 | GEM_HOST_API_KEY: ${{secrets.RUBYGEMS_AUTH_TOKEN}} 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Goldfinger, a WebFinger client for Ruby 2 | ======================================= 3 | 4 | [![Gem Version](http://img.shields.io/gem/v/goldfinger.svg)][gem] 5 | [![Build Status](http://img.shields.io/travis/tootsuite/goldfinger.svg)][travis] 6 | 7 | [gem]: https://rubygems.org/gems/goldfinger 8 | [travis]: https://travis-ci.org/tootsuite/goldfinger 9 | 10 | A WebFinger client for Ruby. Supports `application/xrd+xml` and `application/jrd+json` responses. Raises `Goldfinger::NotFoundError` on failure to fetch the Webfinger or XRD data, can also raise `HTTP:Error` or `OpenSSL::SSL::SSLError` if something is wrong with the HTTPS connection it uses. 11 | 12 | - **Does not** fall back to HTTP if HTTPS is not available 13 | - **Does** check host-meta XRD, but *only* if the standard WebFinger path yielded no result 14 | 15 | ## Installation 16 | 17 | gem install goldfinger 18 | 19 | ## Usage 20 | 21 | data = Goldfinger.finger('acct:gargron@quitter.no') 22 | 23 | data.link('http://schemas.google.com/g/2010#updates-from').href 24 | # => "https://quitter.no/api/statuses/user_timeline/7477.atom" 25 | 26 | data.aliases 27 | # => ["https://quitter.no/user/7477", "https://quitter.no/gargron"] 28 | 29 | data.subject 30 | # => "acct:gargron@quitter.no" 31 | 32 | ## RFC support 33 | 34 | The official WebFinger RFC is [7033](https://tools.ietf.org/html/rfc7033). 35 | -------------------------------------------------------------------------------- /lib/goldfinger/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'addressable' 4 | require 'nokogiri' 5 | 6 | module Goldfinger 7 | class Client 8 | include Goldfinger::Utils 9 | 10 | def initialize(uri, opts = {}) 11 | @uri = uri 12 | @ssl = opts.delete(:ssl) { true } 13 | @scheme = @ssl ? 'https' : 'http' 14 | @opts = opts 15 | end 16 | 17 | def finger 18 | response = perform_get(standard_url, @opts) 19 | 20 | return finger_from_template if response.code != 200 21 | 22 | Goldfinger::Result.new(response) 23 | rescue Addressable::URI::InvalidURIError 24 | raise Goldfinger::NotFoundError, 'Invalid URI' 25 | end 26 | 27 | private 28 | 29 | def finger_from_template 30 | template = perform_get(url, @opts) 31 | 32 | raise Goldfinger::NotFoundError, 'No host-meta on the server' if template.code != 200 33 | 34 | response = perform_get(url_from_template(template.body), @opts) 35 | 36 | raise Goldfinger::NotFoundError, 'No such user on the server' if response.code != 200 37 | 38 | Goldfinger::Result.new(response) 39 | end 40 | 41 | def url 42 | "#{@scheme}://#{domain}/.well-known/host-meta" 43 | end 44 | 45 | def standard_url 46 | "#{@scheme}://#{domain}/.well-known/webfinger?resource=#{@uri}" 47 | end 48 | 49 | def url_from_template(template) 50 | xml = Nokogiri::XML(template) 51 | links = xml.xpath('//xmlns:Link[@rel="lrdd"]') 52 | 53 | raise Goldfinger::NotFoundError if links.empty? 54 | 55 | links.first.attribute('template').value.gsub('{uri}', @uri) 56 | rescue Nokogiri::XML::XPath::SyntaxError 57 | raise Goldfinger::Error, "Bad XML: #{template}" 58 | end 59 | 60 | def domain 61 | @uri.split('@').last 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/goldfinger/link.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Goldfinger 4 | # @!attribute [r] href 5 | # @return [String] The href the link points to 6 | # @!attribute [r] template 7 | # @return [String] The template the link contains 8 | # @!attribute [r] type 9 | # @return [String] The mime type of the link 10 | # @!attribute [r] rel 11 | # @return [String] The relation descriptor of the link 12 | class Link 13 | attr_reader :href, :template, :type, :rel 14 | 15 | def initialize(a) 16 | @href = a[:href] 17 | @template = a[:template] 18 | @type = a[:type] 19 | @rel = a[:rel] 20 | @titles = a[:titles] 21 | @properties = a[:properties] 22 | end 23 | 24 | # The "titles" object comprises zero or more name/value pairs whose 25 | # names are a language tag or the string "und". The string is 26 | # human-readable and describes the link relation. 27 | # @see #title 28 | # @return [Array] Array form of the hash 29 | def titles 30 | @titles.to_a 31 | end 32 | 33 | # The "properties" object within the link relation object comprises 34 | # zero or more name/value pairs whose names are URIs (referred to as 35 | # "property identifiers") and whose values are strings or nil. 36 | # Properties are used to convey additional information about the link 37 | # relation. 38 | # @see #property 39 | # @return [Array] Array form of the hash 40 | def properties 41 | @properties.to_a 42 | end 43 | 44 | # Returns a title for a language 45 | # @param lang [String] 46 | # @return [String] 47 | def title(lang) 48 | @titles[lang] 49 | end 50 | 51 | # Returns a property for a key 52 | # @param key [String] 53 | # @return [String] 54 | def property(key) 55 | @properties[key] 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.3 3 | Exclude: 4 | - 'spec/**/*' 5 | - 'bin/*' 6 | - 'Rakefile' 7 | - 'vendor/**/*' 8 | 9 | Bundler/OrderedGems: 10 | Enabled: false 11 | 12 | Layout/AccessModifierIndentation: 13 | EnforcedStyle: indent 14 | 15 | Layout/EmptyLineAfterMagicComment: 16 | Enabled: false 17 | 18 | Layout/SpaceInsideHashLiteralBraces: 19 | EnforcedStyle: space 20 | 21 | Metrics/AbcSize: 22 | Max: 100 23 | 24 | Metrics/BlockNesting: 25 | Max: 3 26 | 27 | Metrics/ClassLength: 28 | CountComments: false 29 | Max: 200 30 | 31 | Metrics/CyclomaticComplexity: 32 | Max: 15 33 | 34 | Metrics/LineLength: 35 | AllowURI: true 36 | Enabled: false 37 | 38 | Metrics/MethodLength: 39 | CountComments: false 40 | Max: 55 41 | 42 | Metrics/ModuleLength: 43 | CountComments: false 44 | Max: 200 45 | 46 | Metrics/ParameterLists: 47 | Max: 4 48 | CountKeywordArgs: true 49 | 50 | Metrics/PerceivedComplexity: 51 | Max: 10 52 | 53 | Rails: 54 | Enabled: true 55 | 56 | Rails/HasAndBelongsToMany: 57 | Enabled: false 58 | 59 | Rails/SkipsModelValidations: 60 | Enabled: false 61 | 62 | Style/ClassAndModuleChildren: 63 | Enabled: false 64 | 65 | Style/CollectionMethods: 66 | Enabled: true 67 | PreferredMethods: 68 | find_all: 'select' 69 | 70 | Style/Documentation: 71 | Enabled: false 72 | 73 | Style/DoubleNegation: 74 | Enabled: true 75 | 76 | Style/FrozenStringLiteralComment: 77 | Enabled: true 78 | 79 | Style/GuardClause: 80 | Enabled: false 81 | 82 | Style/Lambda: 83 | Enabled: false 84 | 85 | Style/PercentLiteralDelimiters: 86 | PreferredDelimiters: 87 | '%i': '()' 88 | '%w': '()' 89 | 90 | Style/PerlBackrefs: 91 | AutoCorrect: false 92 | 93 | Style/RegexpLiteral: 94 | Enabled: false 95 | 96 | Style/SymbolArray: 97 | Enabled: false 98 | 99 | Style/TrailingCommaInLiteral: 100 | EnforcedStyleForMultiline: 'comma' 101 | -------------------------------------------------------------------------------- /spec/fixtures/quitter.no_.well-known_webfinger.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | acct:gargron@quitter.no 4 | https://quitter.no/user/7477 5 | https://quitter.no/gargron 6 | Bob Smith 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | User Photo 21 | Benutzerfoto 22 | 1970-01-01 23 | 24 | 25 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | goldfinger (2.1.1) 5 | addressable (~> 2.5) 6 | http (~> 4.0) 7 | nokogiri (~> 1.8) 8 | oj (~> 3.0) 9 | 10 | GEM 11 | remote: https://rubygems.org/ 12 | specs: 13 | addressable (2.5.2) 14 | public_suffix (>= 2.0.2, < 4.0) 15 | coderay (1.1.1) 16 | crack (0.4.3) 17 | safe_yaml (~> 1.0.0) 18 | diff-lcs (1.3) 19 | domain_name (0.5.20190701) 20 | unf (>= 0.0.5, < 1.0.0) 21 | ffi (1.12.2) 22 | ffi-compiler (1.0.1) 23 | ffi (>= 1.0.0) 24 | rake 25 | fuubar (2.2.0) 26 | rspec-core (~> 3.0) 27 | ruby-progressbar (~> 1.4) 28 | hashdiff (0.3.4) 29 | http (4.3.0) 30 | addressable (~> 2.3) 31 | http-cookie (~> 1.0) 32 | http-form_data (~> 2.2) 33 | http-parser (~> 1.2.0) 34 | http-cookie (1.0.3) 35 | domain_name (~> 0.5) 36 | http-form_data (2.2.0) 37 | http-parser (1.2.1) 38 | ffi-compiler (>= 1.0, < 2.0) 39 | method_source (0.8.2) 40 | mini_portile2 (2.4.0) 41 | nokogiri (1.10.8) 42 | mini_portile2 (~> 2.4.0) 43 | oj (3.10.2) 44 | pry (0.10.4) 45 | coderay (~> 1.1.0) 46 | method_source (~> 0.8.1) 47 | slop (~> 3.4) 48 | public_suffix (3.0.3) 49 | rake (12.3.3) 50 | rspec (3.6.0) 51 | rspec-core (~> 3.6.0) 52 | rspec-expectations (~> 3.6.0) 53 | rspec-mocks (~> 3.6.0) 54 | rspec-core (3.6.0) 55 | rspec-support (~> 3.6.0) 56 | rspec-expectations (3.6.0) 57 | diff-lcs (>= 1.2.0, < 2.0) 58 | rspec-support (~> 3.6.0) 59 | rspec-mocks (3.6.0) 60 | diff-lcs (>= 1.2.0, < 2.0) 61 | rspec-support (~> 3.6.0) 62 | rspec-support (3.6.0) 63 | ruby-progressbar (1.8.1) 64 | safe_yaml (1.0.4) 65 | slop (3.6.0) 66 | unf (0.1.4) 67 | unf_ext 68 | unf_ext (0.0.7.6) 69 | webmock (3.0.1) 70 | addressable (>= 2.3.6) 71 | crack (>= 0.3.2) 72 | hashdiff 73 | 74 | PLATFORMS 75 | ruby 76 | 77 | DEPENDENCIES 78 | bundler (~> 1.15) 79 | fuubar 80 | goldfinger! 81 | pry (>= 0.10.3) 82 | rake 83 | rspec (>= 3.0) 84 | webmock 85 | 86 | BUNDLED WITH 87 | 1.16.1 88 | -------------------------------------------------------------------------------- /spec/goldfinger/client_spec.rb: -------------------------------------------------------------------------------- 1 | describe Goldfinger::Client do 2 | context 'with HTTPS available' do 3 | describe '#finger' do 4 | before do 5 | stub_request(:get, 'https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no').to_return(body: fixture('quitter.no_.well-known_webfinger.json'), headers: { content_type: 'application/jrd+json' }) 6 | end 7 | 8 | subject { Goldfinger::Client.new('acct:gargron@quitter.no') } 9 | 10 | it 'returns a result' do 11 | expect(subject.finger).to be_instance_of Goldfinger::Result 12 | end 13 | 14 | it 'performs a single HTTP request' do 15 | subject.finger 16 | expect(a_request(:get, 'https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no')).to have_been_made.once 17 | end 18 | end 19 | end 20 | 21 | context 'with only HTTP available' do 22 | describe '#finger' do 23 | before do 24 | stub_request(:get, 'https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no').to_raise(HTTP::Error) 25 | stub_request(:get, 'http://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no').to_return(body: fixture('quitter.no_.well-known_webfinger.json'), headers: { content_type: 'application/jrd+json' }) 26 | end 27 | 28 | subject { Goldfinger::Client.new('acct:gargron@quitter.no') } 29 | 30 | it 'raises an error' do 31 | expect { subject.finger }.to raise_error HTTP::Error 32 | end 33 | end 34 | end 35 | 36 | context 'with XRD missing' do 37 | describe '#finger' do 38 | before do 39 | stub_request(:get, 'https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no').to_raise(HTTP::Error) 40 | stub_request(:get, 'http://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no').to_raise(HTTP::Error) 41 | stub_request(:get, 'https://quitter.no/.well-known/host-meta').to_raise(HTTP::Error) 42 | stub_request(:get, 'http://quitter.no/.well-known/host-meta').to_raise(HTTP::Error) 43 | end 44 | 45 | subject { Goldfinger::Client.new('acct:gargron@quitter.no') } 46 | 47 | it 'raises an error' do 48 | expect { subject.finger }.to raise_error(HTTP::Error) 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/fixtures/quitter.no_.well-known_webfinger.json: -------------------------------------------------------------------------------- 1 | { 2 | "subject": "acct:gargron@quitter.no", 3 | "aliases": [ 4 | "https://quitter.no/user/7477", 5 | "https://quitter.no/gargron" 6 | ], 7 | "properties": { 8 | "http://webfinger.example/ns/name": "Bob Smith" 9 | }, 10 | "links": [ 11 | { 12 | "rel": "http://webfinger.net/rel/profile-page", 13 | "type": "text/html", 14 | "href": "https://quitter.no/gargron" 15 | }, 16 | { 17 | "rel": "http://gmpg.org/xfn/11", 18 | "type": "text/html", 19 | "href": "https://quitter.no/gargron" 20 | }, 21 | { 22 | "rel": "describedby", 23 | "type": "application/rdf+xml", 24 | "href": "https://quitter.no/gargron/foaf" 25 | }, 26 | { 27 | "rel": "http://apinamespace.org/atom", 28 | "type": "application/atomsvc+xml", 29 | "href": "https://quitter.no/api/statusnet/app/service/gargron.xml" 30 | }, 31 | { 32 | "rel": "http://apinamespace.org/twitter", 33 | "href": "https://quitter.no/api/" 34 | }, 35 | { 36 | "rel": "http://specs.openid.net/auth/2.0/provider", 37 | "href": "https://quitter.no/gargron" 38 | }, 39 | { 40 | "rel": "http://schemas.google.com/g/2010#updates-from", 41 | "type": "application/atom+xml", 42 | "href": "https://quitter.no/api/statuses/user_timeline/7477.atom" 43 | }, 44 | { 45 | "rel": "magic-public-key", 46 | "href": "data:application/magic-public-key,RSA.1ZBkHTavLvxH3FzlKv4O6WtlILKRFfNami3_Rcu8EuogtXSYiS-bB6hElZfUCSHbC4uLemOA34PEhz__CDMozax1iI_t8dzjDnh1x0iFSup7pSfW9iXk_WU3Dm74yWWW2jildY41vWgrEstuQ1dJ8vVFfSJ9T_tO4c-T9y8vDI8=.AQAB" 47 | }, 48 | { 49 | "rel": "salmon", 50 | "href": "https://quitter.no/main/salmon/user/7477" 51 | }, 52 | { 53 | "rel": "http://salmon-protocol.org/ns/salmon-replies", 54 | "href": "https://quitter.no/main/salmon/user/7477" 55 | }, 56 | { 57 | "rel": "http://salmon-protocol.org/ns/salmon-mention", 58 | "href": "https://quitter.no/main/salmon/user/7477" 59 | }, 60 | { 61 | "rel": "http://ostatus.org/schema/1.0/subscribe", 62 | "template": "https://quitter.no/main/ostatussub?profile={uri}" 63 | }, 64 | { 65 | "rel": "http://spec.example.net/photo/1.0", 66 | "type": "image/jpeg", 67 | "href": "http://photos.example.com/gpburdell.jpg", 68 | "titles": { 69 | "en": "User Photo", 70 | "de": "Benutzerfoto" 71 | }, 72 | "properties": { 73 | "http://spec.example.net/created/1.0": "1970-01-01" 74 | } 75 | } 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /spec/goldfinger/result_spec.rb: -------------------------------------------------------------------------------- 1 | describe Goldfinger::Result do 2 | shared_examples 'a working finger result' do 3 | subject { Goldfinger::Result.new(response) } 4 | 5 | describe '#links' do 6 | it 'returns a non-empty array' do 7 | expect(subject.links).to be_instance_of Array 8 | expect(subject.links).to_not be_empty 9 | end 10 | end 11 | 12 | describe '#link' do 13 | it 'returns a value for a given rel' do 14 | expect(subject.link('http://webfinger.net/rel/profile-page').href).to eql 'https://quitter.no/gargron' 15 | end 16 | 17 | it 'returns nil if no such link exists' do 18 | expect(subject.link('zzzz')).to be_nil 19 | end 20 | 21 | it 'returns titles map' do 22 | expect(subject.link('http://spec.example.net/photo/1.0').title('en')).to eql 'User Photo' 23 | end 24 | 25 | it 'returns a properties map' do 26 | expect(subject.link('http://spec.example.net/photo/1.0').property('http://spec.example.net/created/1.0')).to eql '1970-01-01' 27 | end 28 | end 29 | 30 | describe '#subject' do 31 | it 'returns the subject' do 32 | expect(subject.subject).to eql 'acct:gargron@quitter.no' 33 | end 34 | end 35 | 36 | describe '#aliases' do 37 | it 'returns a non-empty array' do 38 | expect(subject.aliases).to be_instance_of Array 39 | expect(subject.aliases).to_not be_empty 40 | end 41 | end 42 | 43 | describe '#properties' do 44 | it 'returns an array' do 45 | expect(subject.properties).to be_instance_of Array 46 | expect(subject.properties).to_not be_empty 47 | end 48 | end 49 | 50 | describe '#property' do 51 | it 'returns the value for a key' do 52 | expect(subject.property('http://webfinger.example/ns/name')).to eql 'Bob Smith' 53 | end 54 | 55 | it 'returns nil if no such property exists' do 56 | expect(subject.property('zzzz')).to be_nil 57 | end 58 | end 59 | end 60 | 61 | context 'when the input mime type is application/xrd+xml' do 62 | before do 63 | stub_request(:get, 'https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no').to_return(body: fixture('quitter.no_.well-known_webfinger.xml'), headers: { content_type: 'application/xrd+xml' }) 64 | end 65 | 66 | let(:response) { HTTP.get('https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no') } 67 | 68 | it_behaves_like 'a working finger result' 69 | end 70 | 71 | context 'when the input mime type is application/jrd+json' do 72 | before do 73 | stub_request(:get, 'https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no').to_return(body: fixture('quitter.no_.well-known_webfinger.json'), headers: { content_type: 'application/jrd+json' }) 74 | end 75 | 76 | let(:response) { HTTP.get('https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no') } 77 | 78 | it_behaves_like 'a working finger result' 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/goldfinger/result.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'oj' 4 | 5 | module Goldfinger 6 | # @!attribute [r] subject 7 | # @return [String] URI that identifies the entity that the JRD describes. 8 | # @!attribute [r] aliases 9 | # @return [Array] Zero or more URI strings that identify the same entity as the "subject" URI. 10 | class Result 11 | MIME_TYPES = [ 12 | 'application/jrd+json', 13 | 'application/json', 14 | 'application/xrd+xml', 15 | 'application/xml', 16 | 'text/xml', 17 | ].freeze 18 | 19 | attr_reader :subject, :aliases 20 | 21 | def initialize(response) 22 | @mime_type = response.mime_type 23 | @body = response.body 24 | @subject = nil 25 | @aliases = [] 26 | @links = {} 27 | @properties = {} 28 | 29 | parse 30 | end 31 | 32 | # The "properties" object comprises zero or more name/value pairs whose 33 | # names are URIs (referred to as "property identifiers") and whose 34 | # values are strings or nil. 35 | # @see #property 36 | # @return [Array] Array form of the hash 37 | def properties 38 | @properties.to_a 39 | end 40 | 41 | # Returns a property for a key 42 | # @param key [String] 43 | # @return [String] 44 | def property(key) 45 | @properties[key] 46 | end 47 | 48 | # The "links" array has any number of member objects, each of which 49 | # represents a link. 50 | # @see #link 51 | # @return [Array] Array form of the hash 52 | def links 53 | @links.to_a 54 | end 55 | 56 | # Returns a key for a relation 57 | # @param key [String] 58 | # @return [Goldfinger::Link] 59 | def link(rel) 60 | @links[rel] 61 | end 62 | 63 | private 64 | 65 | def parse 66 | case @mime_type 67 | when 'application/jrd+json', 'application/json' 68 | parse_json 69 | when 'application/xrd+xml', 'application/xml', 'text/xml' 70 | parse_xml 71 | else 72 | raise Goldfinger::Error, "Invalid response mime type: #{@mime_type}" 73 | end 74 | end 75 | 76 | def parse_json 77 | json = Oj.load(@body.to_s, mode: :null) 78 | 79 | @subject = json['subject'] 80 | @aliases = json['aliases'] || [] 81 | @properties = json['properties'] || {} 82 | 83 | json['links'].each do |link| 84 | tmp = Hash[link.keys.map { |key| [key.to_sym, link[key]] }] 85 | @links[link['rel']] = Goldfinger::Link.new(tmp) 86 | end 87 | end 88 | 89 | def parse_xml 90 | xml = Nokogiri::XML(@body) 91 | 92 | @subject = xml.at_xpath('//xmlns:Subject').content 93 | @aliases = xml.xpath('//xmlns:Alias').map(&:content) 94 | 95 | properties = xml.xpath('/xmlns:XRD/xmlns:Property') 96 | properties.each { |prop| @properties[prop.attribute('type').value] = prop.attribute('nil') ? nil : prop.content } 97 | 98 | xml.xpath('//xmlns:Link').each do |link| 99 | rel = link.attribute('rel').value 100 | tmp = Hash[link.attributes.keys.map { |key| [key.to_sym, link.attribute(key).value] }] 101 | 102 | tmp[:titles] = {} 103 | tmp[:properties] = {} 104 | 105 | link.xpath('.//xmlns:Title').each { |title| tmp[:titles][title.attribute('lang').value] = title.content } 106 | link.xpath('.//xmlns:Property').each { |prop| tmp[:properties][prop.attribute('type').value] = prop.attribute('nil') ? nil : prop.content } 107 | 108 | @links[rel] = Goldfinger::Link.new(tmp) 109 | end 110 | end 111 | end 112 | end 113 | --------------------------------------------------------------------------------