├── .ruby-version ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── ci.yml │ └── publish.yml ├── .devcontainer ├── setup.sh └── devcontainer.json ├── spec ├── support │ ├── webmock.rb │ ├── fixtures │ │ ├── example_com.html │ │ ├── example_com_link_element_empty_href.html │ │ ├── example_com_link_element_relative_url.html │ │ ├── example_com_link_element_query_string.html │ │ ├── example_com_multiple_endpoints.html │ │ ├── example_com_link_element_relative_path.html │ │ ├── example_com_link_element_multiple_rel_values.html │ │ ├── example_com_link_element_absolute_url.html │ │ ├── example_com_link_element_no_href.html │ │ ├── example_com_link_element_fragment.html │ │ ├── example_com_link_element_exact_match.html │ │ └── example_com_link_element_html_comment.html │ ├── fixture_helpers.rb │ └── webmention_rocks.rb ├── lib │ └── indieweb │ │ ├── endpoints │ │ ├── client_spec.rb │ │ ├── client_endpoints_spec.rb │ │ └── client_response_spec.rb │ │ └── endpoints_get_spec.rb └── spec_helper.rb ├── Rakefile ├── .irbrc ├── .editorconfig ├── bin ├── rspec ├── rubocop └── console ├── CODE_OF_CONDUCT.md ├── lib └── indieweb │ ├── endpoints │ ├── exceptions.rb │ ├── client.rb │ └── parser.rb │ └── endpoints.rb ├── .rubocop.yml ├── .simplecov ├── Gemfile ├── .git-blame-ignore-revs ├── .gitignore ├── LICENSE ├── indieweb-endpoints.gemspec ├── CONTRIBUTING.md ├── CHANGELOG.md └── README.md /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.4.6 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * jgarber623 2 | -------------------------------------------------------------------------------- /.devcontainer/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | IFS=$'\n\t' 5 | set -vx 6 | 7 | bundle install 8 | -------------------------------------------------------------------------------- /spec/support/webmock.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | WebMock.disable_net_connect!(allow: ["webmention.rocks"]) 4 | -------------------------------------------------------------------------------- /spec/support/fixtures/example_com.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | 5 | require "rspec/core/rake_task" 6 | 7 | RSpec::Core::RakeTask.new 8 | 9 | task default: :spec 10 | -------------------------------------------------------------------------------- /.irbrc: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "irb/completion" 4 | 5 | IRB.conf[:HISTORY_FILE] = ".irb_history" 6 | IRB.conf[:USE_AUTOCOMPLETE] = false 7 | IRB.conf[:USE_PAGER] = false 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_size = 2 8 | indent_style = space 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | assignees: 9 | - "jgarber623" 10 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "rubygems" 5 | require "bundler/setup" 6 | 7 | ARGV.unshift("--require", "spec_helper") 8 | 9 | load Gem.bin_path("rspec-core", "rspec") 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | By contributing to and participating in the development of indieweb-endpoints-ruby, you acknowledge that you have read and agree to the [IndieWeb Code of Conduct](https://indieweb.org/code-of-conduct). 4 | -------------------------------------------------------------------------------- /bin/rubocop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "rubygems" 5 | require "bundler/setup" 6 | 7 | ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) 8 | 9 | load Gem.bin_path("rubocop", "rubocop") 10 | -------------------------------------------------------------------------------- /spec/support/fixture_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FixtureHelpers 4 | def read_fixture(url) 5 | file_name = "#{url.gsub(%r{^https?://}, "").gsub(%r{[/.]}, "_")}.html" 6 | 7 | File.read(File.join(Dir.pwd, "spec", "support", "fixtures", file_name)) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/indieweb/endpoints/exceptions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module IndieWeb 4 | module Endpoints 5 | class Error < StandardError; end 6 | 7 | class HttpError < Error; end 8 | 9 | class InvalidURIError < Error; end 10 | 11 | class SSLError < Error; end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: 2 | - https://rubocop.jgarber.cc/rubocop.yml 3 | - https://rubocop.jgarber.cc/rubocop-rspec.yml 4 | 5 | plugins: rubocop-packaging 6 | 7 | AllCops: 8 | TargetRubyVersion: "2.7" 9 | 10 | # Checks that spec file paths are consistent and well-formed. 11 | RSpec/SpecFilePathFormat: 12 | CustomTransform: 13 | IndieWeb: "indieweb" 14 | -------------------------------------------------------------------------------- /.simplecov: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | formatters = SimpleCov::Formatter.from_env(ENV) 4 | 5 | if RSpec.configuration.files_to_run.length > 1 6 | require "simplecov-console" 7 | 8 | formatters << SimpleCov::Formatter::Console 9 | end 10 | 11 | SimpleCov.start do 12 | enable_coverage :branch 13 | 14 | formatter SimpleCov::Formatter::MultiFormatter.new(formatters) 15 | end 16 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in indieweb-endpoints.gemspec 6 | gemspec 7 | 8 | gem "irb" 9 | gem "rake" 10 | gem "rspec" 11 | gem "rubocop" 12 | gem "rubocop-packaging" 13 | gem "rubocop-performance" 14 | gem "rubocop-rake" 15 | gem "rubocop-rspec" 16 | gem "simplecov" 17 | gem "simplecov-console" 18 | gem "webmock" 19 | -------------------------------------------------------------------------------- /spec/lib/indieweb/endpoints/client_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe IndieWeb::Endpoints::Client do 4 | subject(:client) { described_class.new(url) } 5 | 6 | context "when given invalid arguments" do 7 | let(:url) { "1:" } 8 | 9 | it "raises an IndieWeb::Endpoints::InvalidURIError" do 10 | expect { client }.to raise_error(IndieWeb::Endpoints::InvalidURIError) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/support/fixtures/example_com_link_element_empty_href.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "indieweb/endpoints" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | ENV["IRBRC"] = ".irbrc" 15 | 16 | require "irb" 17 | IRB.start(__FILE__) 18 | -------------------------------------------------------------------------------- /spec/support/fixtures/example_com_link_element_relative_url.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /spec/lib/indieweb/endpoints/client_endpoints_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe IndieWeb::Endpoints::Client, "#endpoints" do 4 | # TODO: Rework these specs to use WebMock: https://github.com/bblimke/webmock 5 | context "when running the webmention.rocks Endpoint Discovery tests" do 6 | WebmentionRocks::ENDPOINT_DISCOVERY_TESTS.each do |url, regexp| 7 | describe url do 8 | subject { described_class.new(url).endpoints[:webmention] } 9 | 10 | it { is_expected.to match(regexp) } 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/support/fixtures/example_com_link_element_query_string.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /spec/support/fixtures/example_com_multiple_endpoints.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /spec/support/fixtures/example_com_link_element_relative_path.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /spec/support/fixtures/example_com_link_element_multiple_rel_values.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /spec/support/fixtures/example_com_link_element_absolute_url.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /spec/support/fixtures/example_com_link_element_no_href.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /lib/indieweb/endpoints.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "http" 4 | require "link-header-parser" 5 | require "nokogiri/html-ext" 6 | 7 | require_relative "endpoints/exceptions" 8 | 9 | require_relative "endpoints/client" 10 | require_relative "endpoints/parser" 11 | 12 | module IndieWeb 13 | module Endpoints 14 | # Discover a URL's IndieAuth, Micropub, Microsub, and Webmention endpoints. 15 | # 16 | # Convenience method for {Client#endpoints}. 17 | # 18 | # @example 19 | # IndieWeb::Endpoints.get("https://aaronparecki.com") 20 | # 21 | # @param (see Client#initialize) 22 | # 23 | # @return (see Client#endpoints) 24 | def self.get(url) 25 | Client.new(url).endpoints 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Since git version 2.23, git-blame has a feature to ignore certain commits. 2 | # 3 | # This file contains a list of commits that are not likely what you are looking 4 | # for in `git blame`. You can set this file as a default ignore file for blame 5 | # by running the following command: 6 | # 7 | # $ git config blame.ignoreRevsFile .git-blame-ignore-revs 8 | 9 | 79f63885decf438df76e211bfc3409ceb6a1b853 10 | 32735aa57b6a08a2e48187e7f3e31fbef442098f 11 | 0f3ca43203c7d68d25554060a9c35d17b5c671da 12 | b5924c963141411f86022a5d039432154c2635d1 13 | 037ef3744eea392abcee4bd200a46d54e2b8427b 14 | c6378d3d3fbeec6b024027bc43807182d4b2fac2 15 | 12614ff03be549e61b6dd0b0e6bf324c2a094903 16 | cb72804ad4cc0380f0e36c17931602d56bbfe924 17 | 98a94d2d76ddfa65a9e2e34d561f83f9ecf64244 18 | 49b30c91e5809649fe580d4da9f76ff6051231e6 19 | -------------------------------------------------------------------------------- /spec/lib/indieweb/endpoints/client_response_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe IndieWeb::Endpoints::Client, "#response" do 4 | subject(:response) { described_class.new(url).response } 5 | 6 | let(:url) { "https://example.com" } 7 | 8 | context "when rescuing from an HTTP::Error" do 9 | it "raises an IndieWeb::Endpoints::HttpError" do 10 | stub_request(:get, url).to_raise(HTTP::Error) 11 | 12 | expect { response }.to raise_error(IndieWeb::Endpoints::HttpError) 13 | end 14 | end 15 | 16 | context "when rescuing from an OpenSSL::SSL::SSLError" do 17 | it "raises an IndieWeb::Endpoints::SSLError" do 18 | stub_request(:get, url).to_raise(OpenSSL::SSL::SSLError) 19 | 20 | expect { response }.to raise_error(IndieWeb::Endpoints::SSLError) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | workflow_call: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | lint: 12 | name: Lint 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v6 16 | - uses: ruby/setup-ruby@v1 17 | with: 18 | bundler-cache: true 19 | - run: bin/rubocop --color 20 | test: 21 | name: Test (Ruby ${{ matrix.ruby }}) 22 | runs-on: ubuntu-latest 23 | needs: lint 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | ruby: ["2.7", "3.0", "3.1", "3.2", "3.3", "3.4"] 28 | steps: 29 | - uses: actions/checkout@v6 30 | - uses: ruby/setup-ruby@v1 31 | with: 32 | bundler-cache: true 33 | ruby-version: ${{ matrix.ruby }} 34 | - run: bin/rspec --force-color --format documentation 35 | -------------------------------------------------------------------------------- /spec/support/fixtures/example_com_link_element_fragment.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /spec/support/fixtures/example_com_link_element_exact_match.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /spec/support/fixtures/example_com_link_element_html_comment.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /.ruby-lsp 5 | /coverage/ 6 | /InstalledFiles 7 | /pkg/ 8 | /spec/reports/ 9 | /spec/examples.txt 10 | /test/tmp/ 11 | /test/version_tmp/ 12 | /tmp/ 13 | 14 | # Used by dotenv library to load environment variables. 15 | # .env 16 | 17 | # Ignore history files. 18 | .irb_history 19 | .rdbg_history 20 | 21 | # Documentation cache and generated files: 22 | /.yardoc/ 23 | /_yardoc/ 24 | /doc/ 25 | /rdoc/ 26 | 27 | # Environment normalization: 28 | /.bundle/ 29 | /vendor/bundle 30 | /lib/bundler/man/ 31 | 32 | # for a library or gem, you might want to ignore these files since the code is 33 | # intended to run in multiple environments; otherwise, check them in: 34 | Gemfile.lock 35 | # .ruby-version 36 | # .ruby-gemset 37 | 38 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 39 | .rvmrc 40 | 41 | # Used by RuboCop. Remote config files pulled in from inherit_from directive. 42 | .rubocop-https?--* 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jason Garber 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/ruby 3 | { 4 | "name": "indieweb-endpoints-ruby", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/base:1-bookworm", 7 | 8 | // Features to add to the dev container. More info: https://containers.dev/features. 9 | "features": { 10 | "ghcr.io/rails/devcontainer/features/ruby:2": { 11 | "version": "3.4.6" 12 | } 13 | }, 14 | 15 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 16 | // "forwardPorts": [], 17 | 18 | // An array of Docker CLI arguments that should be used when running the container. 19 | "runArgs": ["--name", "indieweb-endpoints-ruby"], 20 | 21 | // Use 'postCreateCommand' to run commands after the container is created. 22 | "postCreateCommand": ".devcontainer/setup.sh", 23 | 24 | // Configure tool-specific properties. 25 | "customizations": { 26 | "vscode": { 27 | "extensions": [ 28 | "EditorConfig.EditorConfig" 29 | ] 30 | } 31 | } 32 | 33 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 34 | // "remoteUser": "root" 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | ci: 9 | name: CI 10 | uses: ./.github/workflows/ci.yml 11 | publish-to-rubygems: 12 | name: Publish to RubyGems 13 | permissions: 14 | contents: write 15 | id-token: write 16 | needs: ci 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v6 20 | - uses: ruby/setup-ruby@v1 21 | with: 22 | bundler-cache: true 23 | - uses: rubygems/release-gem@v1 24 | with: 25 | await-release: false 26 | publish-to-github-packages: 27 | name: Publish to GitHub Packages 28 | permissions: 29 | contents: read 30 | packages: write 31 | needs: ci 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v6 35 | - uses: ruby/setup-ruby@v1 36 | with: 37 | bundler-cache: true 38 | - run: | 39 | mkdir -p $HOME/.gem 40 | touch $HOME/.gem/credentials 41 | chmod 0600 $HOME/.gem/credentials 42 | printf -- "---\n:github: Bearer ${{ secrets.GITHUB_TOKEN }}\n" > $HOME/.gem/credentials 43 | - run: bundle exec rake release 44 | env: 45 | BUNDLE_GEM__PUSH_KEY: github 46 | RUBYGEMS_HOST: "https://rubygems.pkg.github.com/${{ github.repository_owner }}" 47 | -------------------------------------------------------------------------------- /indieweb-endpoints.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Gem::Specification.new do |spec| 4 | spec.required_ruby_version = ">= 2.7" 5 | 6 | spec.name = "indieweb-endpoints" 7 | spec.version = "10.0.2" 8 | spec.authors = ["Jason Garber"] 9 | spec.email = ["jason@sixtwothree.org"] 10 | 11 | spec.summary = "Discover a URL’s IndieAuth, Micropub, Microsub, and Webmention endpoints." 12 | spec.description = spec.summary 13 | spec.homepage = "https://github.com/indieweb/indieweb-endpoints-ruby" 14 | spec.license = "MIT" 15 | 16 | spec.files = Dir["lib/**/*"].reject { |f| File.directory?(f) } 17 | spec.files += ["LICENSE", "CHANGELOG.md", "CODE_OF_CONDUCT.md", "CONTRIBUTING.md", "README.md"] 18 | spec.files += ["indieweb-endpoints.gemspec"] 19 | 20 | spec.require_paths = ["lib"] 21 | 22 | spec.metadata = { 23 | "bug_tracker_uri" => "#{spec.homepage}/issues", 24 | "changelog_uri" => "#{spec.homepage}/releases/tag/v#{spec.version}", 25 | "documentation_uri" => "https://rubydoc.info/gems/#{spec.name}/#{spec.version}", 26 | "homepage_uri" => spec.homepage, 27 | "rubygems_mfa_required" => "true", 28 | "source_code_uri" => "#{spec.homepage}/tree/v#{spec.version}", 29 | } 30 | 31 | spec.add_dependency "http", "~> 5.2" 32 | spec.add_dependency "link-header-parser", "~> 7.0", ">= 7.0.1" 33 | spec.add_dependency "nokogiri-html-ext", "~> 1.6" 34 | end 35 | -------------------------------------------------------------------------------- /lib/indieweb/endpoints/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module IndieWeb 4 | module Endpoints 5 | class Client 6 | # Create a new client with a URL to parse for IndieWeb endpoints. 7 | # 8 | # @example 9 | # client = IndieWeb::Endpoints::Client.new("https://aaronparecki.com") 10 | # 11 | # @param url [String, HTTP::URI, #to_s] an absolute URL 12 | # 13 | # @raise [InvalidURIError] 14 | def initialize(url) 15 | @uri = HTTP::URI.parse(url) 16 | rescue Addressable::URI::InvalidURIError => e 17 | raise InvalidURIError, e 18 | end 19 | 20 | # :nocov: 21 | # @return [String] 22 | def inspect 23 | format "#<%s:%#0x @uri=%s>", 24 | class: self.class, 25 | id: object_id << 1, 26 | uri: uri.inspect 27 | end 28 | # :nocov: 29 | 30 | # A Hash of the discovered IndieWeb endpoints from the provided URL. 31 | # 32 | # @return [Hash{Symbol => String, Array, nil}] 33 | def endpoints 34 | @endpoints ||= Parser.new(response).to_h 35 | end 36 | 37 | # The +HTTP::Response+ object. 38 | # 39 | # @return [HTTP::Response] 40 | # 41 | # @raise [HttpError, SSLError] 42 | def response 43 | @response ||= 44 | HTTP 45 | .follow(max_hops: 20) 46 | .headers(accept: "*/*", user_agent: user_agent) 47 | .timeout(connect: 5, read: 5) 48 | .get(uri) 49 | rescue HTTP::Error => e 50 | raise HttpError, e 51 | rescue OpenSSL::SSL::SSLError => e 52 | raise SSLError, e 53 | end 54 | 55 | private 56 | 57 | # @return [HTTP::URI] 58 | attr_reader :uri 59 | 60 | # @return [String] 61 | def user_agent 62 | "Mozilla/5.0 (compatible; IndieWebEndpointsDiscovery/1.0; +https://rubygems.org/gems/indieweb-endpoints)" 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/support/webmention_rocks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module WebmentionRocks 4 | ENDPOINT_DISCOVERY_TESTS = [ 5 | ["https://webmention.rocks/test/1", %r{^https://webmention.rocks/test/1/webmention$}], 6 | ["https://webmention.rocks/test/2", %r{^https://webmention.rocks/test/2/webmention$}], 7 | ["https://webmention.rocks/test/3", %r{^https://webmention.rocks/test/3/webmention$}], 8 | ["https://webmention.rocks/test/4", %r{^https://webmention.rocks/test/4/webmention$}], 9 | ["https://webmention.rocks/test/5", %r{^https://webmention.rocks/test/5/webmention$}], 10 | ["https://webmention.rocks/test/6", %r{^https://webmention.rocks/test/6/webmention$}], 11 | ["https://webmention.rocks/test/7", %r{^https://webmention.rocks/test/7/webmention$}], 12 | ["https://webmention.rocks/test/8", %r{^https://webmention.rocks/test/8/webmention$}], 13 | ["https://webmention.rocks/test/9", %r{^https://webmention.rocks/test/9/webmention$}], 14 | ["https://webmention.rocks/test/10", %r{^https://webmention.rocks/test/10/webmention$}], 15 | ["https://webmention.rocks/test/11", %r{^https://webmention.rocks/test/11/webmention$}], 16 | ["https://webmention.rocks/test/12", %r{^https://webmention.rocks/test/12/webmention$}], 17 | ["https://webmention.rocks/test/13", %r{^https://webmention.rocks/test/13/webmention$}], 18 | ["https://webmention.rocks/test/14", %r{^https://webmention.rocks/test/14/webmention$}], 19 | ["https://webmention.rocks/test/15", %r{^https://webmention.rocks/test/15$}], 20 | ["https://webmention.rocks/test/16", %r{^https://webmention.rocks/test/16/webmention$}], 21 | ["https://webmention.rocks/test/17", %r{^https://webmention.rocks/test/17/webmention$}], 22 | ["https://webmention.rocks/test/18", %r{^https://webmention.rocks/test/18/webmention$}], 23 | ["https://webmention.rocks/test/19", %r{^https://webmention.rocks/test/19/webmention$}], 24 | ["https://webmention.rocks/test/20", %r{^https://webmention.rocks/test/20/webmention$}], 25 | ["https://webmention.rocks/test/21", %r{^https://webmention.rocks/test/21/webmention\?query=yes$}], 26 | ["https://webmention.rocks/test/22", %r{^https://webmention.rocks/test/22/webmention$}], 27 | ["https://webmention.rocks/test/23/page", %r{^https://webmention.rocks/test/23/page/webmention-endpoint/.*$}], 28 | ].freeze 29 | end 30 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to indieweb-endpoints-ruby 2 | 3 | There are a couple ways you can help improve indieweb-endpoints-ruby: 4 | 5 | 1. Fix an existing [Issue][issues] and submit a [Pull Request][pulls]. 6 | 1. Review open [Pull Requests][pulls]. 7 | 1. Report a new [Issue][issues]. _Only do this after you've made sure the behavior or problem you're observing isn't already documented in an open issue._ 8 | 9 | ## Getting Started 10 | 11 | indieweb-endpoints-ruby is developed using Ruby 3.4 and is tested against additional Ruby versions using [GitHub Actions](https://github.com/indieweb/indieweb-endpoints-ruby/actions). 12 | 13 | > [!TIP] 14 | > This project is configured with a [Dev Container](https://containers.dev) which includes everything you'd need to contribute to indieweb-endpoints-ruby. If you use a supported code editor or IDE, you're encouraged to use the existing Dev Container setup. 15 | 16 | Before making changes to indieweb-endpoints-ruby, you'll want to install Ruby 3.4. Using a Ruby version managment tool like [rbenv](https://github.com/rbenv/rbenv), [chruby](https://github.com/postmodern/chruby), or [rvm](https://github.com/rvm/rvm) is recommended. Once you've installed Ruby 3.4 using your method of choice, install the project's gems by running: 17 | 18 | ```sh 19 | bundle install 20 | ``` 21 | 22 | ## Making Changes 23 | 24 | 1. Fork and clone the project's repo. 25 | 2. Install development dependencies as outlined above. 26 | 3. Create a feature branch for the code changes you're looking to make: `git checkout -b my-new-feature`. 27 | 4. _Write some code!_ 28 | 5. If your changes would benefit from testing, add the necessary tests and verify everything passes by running `bin/rspec`. 29 | 6. Commit your changes: `git commit -am 'Add some new feature or fix some issue'`. _(See [this excellent article](https://cbea.ms/git-commit/) for tips on writing useful Git commit messages.)_ 30 | 7. Push the branch to your fork: `git push -u origin my-new-feature`. 31 | 8. Create a new [Pull Request][pulls] and we'll review your changes. 32 | 33 | ## Code Style 34 | 35 | Code formatting conventions are defined in the `.editorconfig` file which uses the [EditorConfig](https://editorconfig.org) syntax. There are [plugins for a variety of editors](https://editorconfig.org/#download) that utilize the settings in the `.editorconfig` file. We recommended installing the EditorConfig plugin for your editor of choice. 36 | 37 | Your bug fix or feature addition won't be rejected if it runs afoul of any (or all) of these guidelines, but following the guidelines will definitely make everyone's lives a little easier. 38 | 39 | [issues]: https://github.com/indieweb/indieweb-endpoints-ruby/issues 40 | [pulls]: https://github.com/indieweb/indieweb-endpoints-ruby/pulls 41 | -------------------------------------------------------------------------------- /lib/indieweb/endpoints/parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module IndieWeb 4 | module Endpoints 5 | # @api private 6 | class Parser 7 | # @param response [HTTP::Response] 8 | def initialize(response) 9 | @response = response 10 | end 11 | 12 | # @param identifier [String] 13 | # @param node_names [Array] 14 | # 15 | # @return [Array] 16 | # 17 | # @raise [InvalidURIError] 18 | def matches(identifier, node_names: ["link"]) 19 | results = (matches_from_headers(identifier) + matches_from_body(identifier, node_names)).compact 20 | 21 | results.uniq! 22 | results.sort! 23 | 24 | results 25 | end 26 | 27 | # @param (see #matches) 28 | # 29 | # @return [String] 30 | def match(identifier, **kwargs) 31 | matches(identifier, **kwargs).first 32 | end 33 | 34 | # @return [Hash{Symbol => String, Array, nil}] 35 | def to_h 36 | { 37 | authorization_endpoint: match("authorization_endpoint"), 38 | "indieauth-metadata": match("indieauth-metadata"), 39 | micropub: match("micropub"), 40 | microsub: match("microsub"), 41 | redirect_uri: (redirect_uri = matches("redirect_uri")).any? ? redirect_uri : nil, 42 | token_endpoint: match("token_endpoint"), 43 | webmention: match("webmention", node_names: ["link", "a"]), 44 | } 45 | end 46 | 47 | private 48 | 49 | # @return [HTTP::Response] 50 | attr_reader :response 51 | 52 | # @return [Nokogiri::HTML5::Document] 53 | def body 54 | @body ||= Nokogiri::HTML5(response.body, response.uri.to_s).resolve_urls! 55 | end 56 | 57 | # @return [Hash{Symbol => Array}] 58 | def headers 59 | @headers ||= LinkHeaderParser.parse(response.headers.get("link"), base_uri: response.uri).group_by_relation_type 60 | end 61 | 62 | # Reject URLs with fragment identifiers per the IndieAuth specification. 63 | # 64 | # @param identifier [String, #to_s] 65 | # @param node_names [Array] 66 | # 67 | # @return [Array] 68 | def matches_from_body(identifier, node_names) 69 | return [] unless response.mime_type == "text/html" 70 | 71 | body 72 | .css(*node_names.map { |node| %(#{node}[rel~="#{identifier}"][href]:not([href*="#"]) / @href) }) 73 | .map(&:value) 74 | end 75 | 76 | # Reject URLs with fragment identifiers per the IndieAuth specification. 77 | # 78 | # @param identifier [String, #to_sym] 79 | # 80 | # @return [Array] 81 | def matches_from_headers(identifier) 82 | Array(headers[identifier]).filter_map do |header| 83 | header.target_uri unless HTTP::URI.parse(header.target_uri).fragment 84 | end 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | > [!NOTE] 4 | > From v9.0.0, changes are documented using [GitHub Releases](https://github.com/indieweb/indieweb-endpoints-ruby/releases). For a given release, metadata on RubyGems.org will link to that version's Release page. 5 | 6 | ## 8.0.0 / 2022-11-09 7 | 8 | - Refactor `ResponseHeadersParser#results_for` to use `Enumerable#filter_map` (946ff3d) 9 | - Update link-header-parser dependency constraint (4f093fb) 10 | - **Breaking change:** Update development Ruby to 2.7.6 and minimum Ruby to 2.7 (593455d) 11 | 12 | ## 7.2.0 / 2022-10-04 13 | 14 | - Add support for `indieauth-metadata` endpoint (35cc950) 15 | - Switch from pry-byebug to Ruby's debug gem (7ad8925) 16 | - Update development Ruby version to 2.6.10 (f105752) 17 | 18 | ## 7.1.0 / 2022-03-08 19 | 20 | - Refactor gem code (eba8115) 21 | - Refactor specs (5b92119) 22 | - Rescue from `OpenSSL::SSL::SSLError` (4ea38e5) 23 | 24 | ## 7.0.0 / 2022-01-06 25 | 26 | - Update runtime dependency versions (5c9430f) 27 | - **Breaking change:** Update development Ruby version to 2.6.9 and minimum Ruby version to 2.6 (ed17ab1 and 7e4a621) 28 | - Migrate to GitHub Actions from Travis CI (c019756) 29 | - Add `rubygems_mfa_required` to gemspec metadata (1f0a40f) 30 | 31 | ## 6.1.0 / 2021-05-25 32 | 33 | - Add support for Ruby 3.0 (3394252) 34 | 35 | ## 6.0.0 / 2021-05-25 36 | 37 | - Refactor parsers (e22e0af) 38 | - Simplify exception handling (65361ee) 39 | - Update http dependency to 5.0 (44f2d23) 40 | - **Breaking change:** Favor Addressable::URI.join over Absolutely (0bc5049) 41 | - Update development Ruby version to 2.5.9 (3439cce) 42 | 43 | ## 5.0.0 / 2020-11-11 44 | 45 | - Update absolutely and link-header-parser dependencies (064696e) 46 | 47 | ## 4.0.0 / 2020-07-21 48 | 49 | - **Breaking change:** Return a Hash of endpoints instead of an OpenStruct (15dc387) 50 | - Update [link-header-parser](https://rubygems.org/gems/link-header-parser) dependency to v2.0.0 (2255e6b) 51 | - **Breaking change:** Update development Ruby version to 2.5.8 and minimum Ruby version to 2.5 (dd5a142) 52 | - Refactor response headers/body parsers into a single class (ee02da3) 53 | - Refactor `IndieWeb::Endpoints::Client` and remove `HttpRequestService` (2732616) 54 | - Add offending url to exception message (#5) (4bf7a54) 55 | 56 | ## 3.0.0 / 2020-05-14 57 | 58 | - Update Absolutely and LinkHeaderParser dependencies (9e0a64a) 59 | - Move development dependencies to `Gemfile` (67067f3) 60 | - Update development Ruby version to 2.4.10 (f5430f9) 61 | 62 | ## 2.0.0 / 2020-01-25 63 | 64 | - Downgrade HTTP gem version constraint to ~> 4.3 (cb63230) 65 | 66 | ## 1.1.0 / 2020-01-20 67 | 68 | - Expand supported Ruby versions to include 2.7 (ae63ed0) 69 | - Update project Ruby version to 2.4.9 (e576ad6) 70 | 71 | ## 1.0.2 / 2019-08-31 72 | 73 | - Update WebMock and Addressable gems (298da63) 74 | - Update development dependencies (5663e3a) 75 | 76 | ## 1.0.1 / 2019-07-17 77 | 78 | - Safely combine, flatten, and compact result set (5f2d9c5) 79 | 80 | ## 1.0.0 / 2019-07-08 81 | 82 | - Refactor gem code using service objects approach (78511d7 and 9d2fee0) 83 | 84 | ## 0.7.0 / 2019-07-03 85 | 86 | - Update runtime and development dependencies (d99214b and 7692ab3) 87 | 88 | ## 0.6.0 / 2019-06-15 89 | 90 | - Update Parser-related classes to resolve #2 91 | 92 | ## 0.5.0 / 2019-05-09 93 | 94 | - Add support for Microsub endpoint discovery (5e81d9f) 95 | - Refactor parsers to ignore URLs with fragments (797b376) 96 | - Rescue `NoMethodError` (for `nil`) and `TypeError` (for non-`String`) (e33522e) 97 | - Raise `ArgumentError` if url scheme is not `http` or `https` (8eb1b1a) 98 | - Shorten up User Agent string (f9717b4) 99 | - Refactor `HTTPRequest` class using specification defaults (feef2ba) 100 | 101 | ## 0.4.0 / 2019-05-01 102 | 103 | - Add `IndieWeb::Endpoints.client` method (c4d42d0) 104 | - Rename base `Error` class to `IndieWebEndpointsError` (d6d6f98) 105 | - Add `HttpRequest` class (7864cbd) 106 | 107 | ## 0.3.0 / 2019-04-30 108 | 109 | - `IndieWeb::Endpoints::Client#endpoints` returns an `OpenStruct` instead of a `Hash` (c209b0b). 110 | 111 | ## 0.2.0 / 2019-04-25 112 | 113 | - Subclass exceptions under `IndieWeb::Endpoints::Error` (667eec7) 114 | - Refactor parsers and `Registerable` module (3b96858) 115 | - Refactor `Client#response` method (c36fda3) 116 | 117 | ## 0.1.0 / 2019-04-24 118 | 119 | - Initial release! 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # indieweb-endpoints-ruby 2 | 3 | **A Ruby gem for discovering a URL's [IndieAuth](https://indieweb.org/IndieAuth), [Micropub](https://indieweb.org/Micropub), [Microsub](https://indieweb.org/Microsub), and [Webmention](https://indieweb.org/Webmention) endpoints.** 4 | 5 | [![Gem](https://img.shields.io/gem/v/indieweb-endpoints.svg?logo=rubygems&style=for-the-badge)](https://rubygems.org/gems/indieweb-endpoints) 6 | [![Downloads](https://img.shields.io/gem/dt/indieweb-endpoints.svg?logo=rubygems&style=for-the-badge)](https://rubygems.org/gems/indieweb-endpoints) 7 | [![Build](https://img.shields.io/github/actions/workflow/status/indieweb/indieweb-endpoints-ruby/ci.yml?branch=main&logo=github&style=for-the-badge)](https://github.com/indieweb/indieweb-endpoints-ruby/actions/workflows/ci.yml) 8 | 9 | ## Key Features 10 | 11 | - Compliant with [Section 4.1](https://www.w3.org/TR/indieauth/#discovery-by-clients) and [Section 4.2.2](https://www.w3.org/TR/indieauth/#redirect-url) of [the W3C's IndieAuth Working Group Note](https://www.w3.org/TR/indieauth/), [Section 5.3](https://www.w3.org/TR/micropub/#endpoint-discovery) of [the W3C's Micropub Recommendation](https://www.w3.org/TR/micropub/), and [Section 3.1.2](https://www.w3.org/TR/webmention/#sender-discovers-receiver-webmention-endpoint) of [the W3C's Webmention Recommendation](https://www.w3.org/TR/webmention/). 12 | - Passes all Endpoint Discovery tests on [webmention.rocks](https://webmention.rocks). 13 | - Supports Ruby 2.7 and newer. 14 | 15 | ## Getting Started 16 | 17 | Before installing and using indieweb-endpoints-ruby, you'll want to have [Ruby](https://www.ruby-lang.org) 2.7 (or newer) installed. Using a Ruby version managment tool like [rbenv](https://github.com/rbenv/rbenv), [chruby](https://github.com/postmodern/chruby), or [rvm](https://github.com/rvm/rvm) is recommended. 18 | 19 | indieweb-endpoints-ruby is developed using Ruby 3.4 and is tested against additional Ruby versions using [GitHub Actions](https://github.com/indieweb/indieweb-endpoints-ruby/actions). 20 | 21 | ## Installation 22 | 23 | Add indieweb-endpoints-ruby to your project's `Gemfile` and run `bundle install`: 24 | 25 | ```ruby 26 | source "https://rubygems.org" 27 | 28 | gem "indieweb-endpoints" 29 | ``` 30 | 31 | ## Usage 32 | 33 | ### Basic Usage 34 | 35 | With indieweb-endpoints-ruby added to your project's `Gemfile` and installed, you may discover a URL's IndieWeb-relevant endpoints by doing: 36 | 37 | ```ruby 38 | require "indieweb/endpoints" 39 | 40 | IndieWeb::Endpoints.get("https://aaronparecki.com") 41 | #=> { authorization_endpoint: "https://aaronparecki.com/auth", "indieauth-metadata": "https://aaronparecki.com/.well-known/oauth-authorization-server", micropub: "https://aaronparecki.com/micropub", microsub: "https://aperture.p3k.io/microsub/1", redirect_uri: nil, token_endpoint: "https://aaronparecki.com/auth/token", webmention: "https://webmention.io/aaronpk/webmention" } 42 | ``` 43 | 44 | This example will search [Aaron's website](https://aaronparecki.com) for valid IndieAuth, Micropub, and Webmention endpoints and return a `Hash` of results. Each key in the returned `Hash` will have a value of either a `String` representing a URL or `nil`. The `redirect_uri` key's value will be either an `Array` or `nil` since a given URL may register multiple callback URLs. 45 | 46 | ### Advanced Usage 47 | 48 | Should the need arise, you may work with the `IndieWeb::Endpoints::Client` class: 49 | 50 | ```ruby 51 | require "indieweb/endpoints" 52 | 53 | client = IndieWeb::Endpoints::Client.new("https://aaronparecki.com") 54 | #=> # 55 | 56 | client.response 57 | #=> # 58 | 59 | client.endpoints 60 | #=> { authorization_endpoint: "https://aaronparecki.com/auth", micropub: "https://aaronparecki.com/micropub", microsub: "https://aperture.p3k.io/microsub/1", redirect_uri: nil, token_endpoint: "https://aaronparecki.com/auth/token", webmention: "https://webmention.io/aaronpk/webmention" } 61 | ``` 62 | 63 | ### Exception Handling 64 | 65 | indieweb-endpoints-ruby may raise the following exceptions which are subclasses of `IndieWeb::Endpoints::Error` (which itself is a subclass of `StandardError`). 66 | 67 | - `IndieWeb::Endpoints::InvalidURIError` 68 | - `IndieWeb::Endpoints::HttpError` 69 | - `IndieWeb::Endpoints::SSLError` 70 | 71 | ## Contributing 72 | 73 | See [CONTRIBUTING.md](https://github.com/indieweb/indieweb-endpoints-ruby/blob/main/CONTRIBUTING.md) for more on how to contribute to indieweb-endpoints-ruby. Your help is greatly appreciated! 74 | 75 | By contributing to and participating in the development of indieweb-endpoints-ruby, you acknowledge that you have read and agree to the [IndieWeb Code of Conduct](https://indieweb.org/code-of-conduct). 76 | 77 | ## Acknowledgments 78 | 79 | indieweb-endpoints-ruby wouldn't exist without IndieAuth, Micropub, and Webmention and the hard work put in by everyone involved in the [IndieWeb](https://indieweb.org) movement. Additionally, the comprehensive Webmention Endpoint Discovery test suite at [webmention.rocks](https://webmention.rocks) was invaluable in the development of this Ruby gem. 80 | 81 | indieweb-endpoints-ruby is written and maintained by [Jason Garber](https://sixtwothree.org). 82 | 83 | ## License 84 | 85 | indieweb-endpoints-ruby is freely available under the [MIT License](https://opensource.org/licenses/MIT). 86 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file was generated by the `rspec --init` command. Conventionally, all 4 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 5 | # The generated `.rspec` file contains `--require spec_helper` which will cause 6 | # this file to always be loaded, without a need to explicitly require it in any 7 | # files. 8 | # 9 | # Given that it is always loaded, you are encouraged to keep this file as 10 | # light-weight as possible. Requiring heavyweight dependencies from this file 11 | # will add to the boot time of your test suite on EVERY test run, even for an 12 | # individual file that may not need all of that loaded. Instead, consider making 13 | # a separate helper file that requires the additional dependencies and performs 14 | # the additional setup, and require it from the spec files that actually need 15 | # it. 16 | # 17 | # See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 18 | 19 | require "simplecov" 20 | require "webmock/rspec" 21 | 22 | require "indieweb/endpoints" 23 | 24 | Dir.glob("support/**/*.rb", base: __dir__).sort.each { |f| require_relative f } 25 | 26 | RSpec.configure do |config| 27 | config.include FixtureHelpers 28 | 29 | # rspec-expectations config goes here. You can use an alternate 30 | # assertion/expectation library such as wrong or the stdlib/minitest 31 | # assertions if you prefer. 32 | config.expect_with :rspec do |expectations| 33 | # This option will default to `true` in RSpec 4. It makes the `description` 34 | # and `failure_message` of custom matchers include text for helper methods 35 | # defined using `chain`, e.g.: 36 | # be_bigger_than(2).and_smaller_than(4).description 37 | # # => "be bigger than 2 and smaller than 4" 38 | # ...rather than: 39 | # # => "be bigger than 2" 40 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 41 | end 42 | 43 | # rspec-mocks config goes here. You can use an alternate test double 44 | # library (such as bogus or mocha) by changing the `mock_with` option here. 45 | config.mock_with :rspec do |mocks| 46 | # Prevents you from mocking or stubbing a method that does not exist on 47 | # a real object. This is generally recommended, and will default to 48 | # `true` in RSpec 4. 49 | mocks.verify_partial_doubles = true 50 | end 51 | 52 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 53 | # have no way to turn it off -- the option exists only for backwards 54 | # compatibility in RSpec 3). It causes shared context metadata to be 55 | # inherited by the metadata hash of host groups and examples, rather than 56 | # triggering implicit auto-inclusion in groups with matching metadata. 57 | config.shared_context_metadata_behavior = :apply_to_host_groups 58 | 59 | # The settings below are suggested to provide a good initial experience 60 | # with RSpec, but feel free to customize to your heart's content. 61 | 62 | # This allows you to limit a spec run to individual examples or groups 63 | # you care about by tagging them with `:focus` metadata. When nothing 64 | # is tagged with `:focus`, all examples get run. RSpec also provides 65 | # aliases for `it`, `describe`, and `context` that include `:focus` 66 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 67 | config.filter_run_when_matching :focus 68 | 69 | # Allows RSpec to persist some state between runs in order to support 70 | # the `--only-failures` and `--next-failure` CLI options. We recommend 71 | # you configure your source control system to ignore this file. 72 | # config.example_status_persistence_file_path = 'spec/examples.txt' 73 | 74 | # Limits the available syntax to the non-monkey patched syntax that is 75 | # recommended. For more details, see: 76 | # https://relishapp.com/rspec/rspec-core/docs/configuration/zero-monkey-patching-mode 77 | config.disable_monkey_patching! 78 | 79 | # This setting enables warnings. It's recommended, but in some cases may 80 | # be too noisy due to issues in dependencies. 81 | config.warnings = true 82 | 83 | # Many RSpec users commonly either run the entire suite or an individual 84 | # file, and it's useful to allow more verbose output when running an 85 | # individual spec file. 86 | if config.files_to_run.one? 87 | # Use the documentation formatter for detailed output, 88 | # unless a formatter has already been configured 89 | # (e.g. via a command-line flag). 90 | config.default_formatter = "doc" 91 | end 92 | 93 | # Print the 10 slowest examples and example groups at the 94 | # end of the spec run, to help surface which specs are running 95 | # particularly slow. 96 | # config.profile_examples = 10 97 | 98 | # Run specs in random order to surface order dependencies. If you find an 99 | # order dependency and want to debug it, you can fix the order by providing 100 | # the seed, which is printed after each run. 101 | # --seed 1234 102 | config.order = :random 103 | 104 | # Seed global randomization in this process using the `--seed` CLI option. 105 | # Setting this allows you to use `--seed` to deterministically reproduce 106 | # test failures related to randomization by passing the same `--seed` value 107 | # as the one that triggered the failure. 108 | Kernel.srand config.seed 109 | end 110 | -------------------------------------------------------------------------------- /spec/lib/indieweb/endpoints_get_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_examples "a Hash of endpoints" do 4 | subject { described_class.get(url) } 5 | 6 | before do 7 | stub_request(:get, url).to_return(response) 8 | end 9 | 10 | it { is_expected.to eq(endpoints) } 11 | end 12 | 13 | RSpec.describe IndieWeb::Endpoints, ".get" do 14 | context "when given a URL that publishes no endpoints" do 15 | it_behaves_like "a Hash of endpoints" do 16 | let(:url) { "https://example.com" } 17 | 18 | let(:response) do 19 | { 20 | headers: { 21 | "Content-Type": "text/html", 22 | }, 23 | body: read_fixture(url), 24 | } 25 | end 26 | 27 | let(:endpoints) do 28 | { 29 | authorization_endpoint: nil, 30 | "indieauth-metadata": nil, 31 | micropub: nil, 32 | microsub: nil, 33 | redirect_uri: nil, 34 | token_endpoint: nil, 35 | webmention: nil, 36 | } 37 | end 38 | end 39 | end 40 | 41 | context "when given a URL that publishes endpoints in HTTP headers" do 42 | let(:url) { "https://example.com" } 43 | 44 | let(:endpoints) do 45 | { 46 | authorization_endpoint: "https://example.com/authorization_endpoint", 47 | "indieauth-metadata": "https://example.com/indieauth-metadata", 48 | micropub: "https://example.com/micropub", 49 | microsub: "https://example.com/microsub", 50 | redirect_uri: ["https://example.com/redirect_uri"], 51 | token_endpoint: "https://example.com/token_endpoint", 52 | webmention: "https://example.com/webmention", 53 | } 54 | end 55 | 56 | # Similar to https://webmention.rocks/test/1 57 | context "when the HTTP Link header references a relative URL and the `rel` parameter is unquoted" do 58 | it_behaves_like "a Hash of endpoints" do 59 | let(:response) do 60 | { 61 | headers: { 62 | Link: [ 63 | "; rel=authorization_endpoint", 64 | "; rel=indieauth-metadata", 65 | "; rel=micropub", 66 | "; rel=microsub", 67 | "; rel=redirect_uri", 68 | "; rel=token_endpoint", 69 | "; rel=webmention", 70 | ], 71 | }, 72 | } 73 | end 74 | end 75 | end 76 | 77 | # Similar to https://webmention.rocks/test/2 78 | context "when the HTTP Link header references an absolute URL and the `rel` parameter is unquoted" do 79 | it_behaves_like "a Hash of endpoints" do 80 | let(:url) { "https://example.com" } 81 | 82 | let(:response) do 83 | { 84 | headers: { 85 | Link: [ 86 | %(<#{url}/authorization_endpoint>; rel=authorization_endpoint), 87 | %(<#{url}/indieauth-metadata>; rel=indieauth-metadata), 88 | %(<#{url}/micropub>; rel=micropub), 89 | %(<#{url}/microsub>; rel=microsub), 90 | %(<#{url}/redirect_uri>; rel=redirect_uri), 91 | %(<#{url}/token_endpoint>; rel=token_endpoint), 92 | %(<#{url}/webmention>; rel=webmention), 93 | ], 94 | }, 95 | } 96 | end 97 | end 98 | end 99 | 100 | # Similar to https://webmention.rocks/test/7 101 | context "when the HTTP Link header has strange casing" do 102 | it_behaves_like "a Hash of endpoints" do 103 | let(:url) { "https://example.com" } 104 | 105 | let(:response) do 106 | { 107 | headers: { 108 | "LinK" => [ 109 | %(<#{url}/authorization_endpoint>; rel=authorization_endpoint), 110 | %(<#{url}/indieauth-metadata>; rel=indieauth-metadata), 111 | %(<#{url}/micropub>; rel=micropub), 112 | %(<#{url}/microsub>; rel=microsub), 113 | %(<#{url}/redirect_uri>; rel=redirect_uri), 114 | %(<#{url}/token_endpoint>; rel=token_endpoint), 115 | %(<#{url}/webmention>; rel=webmention), 116 | ], 117 | }, 118 | } 119 | end 120 | end 121 | end 122 | 123 | # Similar to https://webmention.rocks/test/8 124 | context "when the `rel` parameter is quoted" do 125 | it_behaves_like "a Hash of endpoints" do 126 | let(:response) do 127 | { 128 | headers: { 129 | Link: [ 130 | %(<#{url}/authorization_endpoint>; rel="authorization_endpoint"), 131 | %(<#{url}/indieauth-metadata>; rel="indieauth-metadata"), 132 | %(<#{url}/micropub>; rel="micropub"), 133 | %(<#{url}/microsub>; rel="microsub"), 134 | %(<#{url}/redirect_uri>; rel="redirect_uri"), 135 | %(<#{url}/token_endpoint>; rel="token_endpoint"), 136 | %(<#{url}/webmention>; rel="webmention"), 137 | ], 138 | }, 139 | } 140 | end 141 | end 142 | end 143 | 144 | # Similar to https://webmention.rocks/test/10 145 | context "when the `rel` parameter contains multiple space-separated values" do 146 | it_behaves_like "a Hash of endpoints" do 147 | let(:response) do 148 | { 149 | headers: { 150 | Link: [ 151 | %(<#{url}/authorization_endpoint>; rel="authorization_endpoint somethingelse"), 152 | %(<#{url}/indieauth-metadata>; rel="indieauth-metadata somethingelse"), 153 | %(<#{url}/micropub>; rel="micropub somethingelse"), 154 | %(<#{url}/microsub>; rel="microsub somethingelse"), 155 | %(<#{url}/redirect_uri>; rel="redirect_uri somethingelse"), 156 | %(<#{url}/token_endpoint>; rel="token_endpoint somethingelse"), 157 | %(<#{url}/webmention>; rel="webmention somethingelse"), 158 | ], 159 | }, 160 | } 161 | end 162 | end 163 | end 164 | 165 | # Similar to https://webmention.rocks/test/18 166 | context "when the response includes multiple HTTP Link headers" do 167 | it_behaves_like "a Hash of endpoints" do 168 | let(:response) do 169 | { 170 | headers: { 171 | Link: [ 172 | %(<#{url}/authorization_endpoint#error>; rel="authorization_endpoint"), 173 | %(; rel="authorization_endpoint_error"), 174 | %(<#{url}/authorization_endpoint>; rel="authorization_endpoint"), 175 | '; rel="other"', 176 | %(<#{url}/redirect_uri>; rel="redirect_uri"), 177 | '; rel="redirect_uri"', 178 | ], 179 | }, 180 | } 181 | end 182 | 183 | let(:endpoints) do 184 | { 185 | authorization_endpoint: "https://example.com/authorization_endpoint", 186 | "indieauth-metadata": nil, 187 | micropub: nil, 188 | microsub: nil, 189 | redirect_uri: ["https://example.com/callback", "https://example.com/redirect_uri"], 190 | token_endpoint: nil, 191 | webmention: nil, 192 | } 193 | end 194 | end 195 | end 196 | 197 | # Similar to https://webmention.rocks/test/19 198 | context "when the HTTP Link header contains multiple comma-separated values" do 199 | it_behaves_like "a Hash of endpoints" do 200 | let(:response) do 201 | { 202 | headers: { 203 | Link: [ 204 | %(; rel="other", \ 205 | <#{url}/authorization_endpoint>; rel="authorization_endpoint"), 206 | %(; rel="other", ; rel="indieauth-metadata"), 207 | %(; rel="other", <#{url}/micropub>; rel="micropub"), 208 | %(; rel="other", <#{url}/microsub>; rel="microsub"), 209 | %(; rel="other", <#{url}/redirect_uri>; rel="redirect_uri"), 210 | %(; rel="other", <#{url}/token_endpoint>; rel="token_endpoint"), 211 | %(; rel="other", <#{url}/webmention>; rel="webmention"), 212 | ], 213 | }, 214 | } 215 | end 216 | end 217 | end 218 | 219 | # Similar to https://webmention.rocks/test/23 220 | context "when the HTTP Link header redirects to a relative URL" do 221 | it_behaves_like "a Hash of endpoints" do 222 | let(:response) do 223 | { 224 | headers: { 225 | Location: "page/authorization_endpoint", 226 | }, 227 | status: 302, 228 | } 229 | end 230 | 231 | let(:endpoints) do 232 | { 233 | authorization_endpoint: "https://example.com/page/authorization_endpoint/endpoint", 234 | "indieauth-metadata": nil, 235 | micropub: nil, 236 | microsub: nil, 237 | redirect_uri: nil, 238 | token_endpoint: nil, 239 | webmention: nil, 240 | } 241 | end 242 | 243 | # Note that this executes after the shared example's before block 244 | before do 245 | redirected_url = "https://example.com/page/authorization_endpoint" 246 | 247 | stub_request(:get, redirected_url).to_return( 248 | headers: { 249 | "Content-Type": "text/html", 250 | Link: "<#{redirected_url}/endpoint>; rel=authorization_endpoint", 251 | } 252 | ) 253 | end 254 | end 255 | end 256 | end 257 | 258 | context "when given a URL that publishes endpoints in HTML elements" do 259 | let(:response) do 260 | { 261 | headers: { 262 | "Content-Type": "text/html", 263 | }, 264 | body: read_fixture(url), 265 | } 266 | end 267 | 268 | let(:endpoints) do 269 | { 270 | authorization_endpoint: "https://example.com/authorization_endpoint", 271 | "indieauth-metadata": "https://example.com/indieauth-metadata", 272 | micropub: "https://example.com/micropub", 273 | microsub: "https://example.com/microsub", 274 | redirect_uri: ["https://example.com/redirect"], 275 | token_endpoint: "https://example.com/token_endpoint", 276 | webmention: nil, 277 | } 278 | end 279 | 280 | # Similar to https://webmention.rocks/test/3 281 | context "when the `link` element references a relative URL" do 282 | it_behaves_like "a Hash of endpoints" do 283 | let(:url) { "https://example.com/link_element_relative_url" } 284 | end 285 | end 286 | 287 | # Similar to https://webmention.rocks/test/4 288 | context "when the `link` element references an absolute URL" do 289 | it_behaves_like "a Hash of endpoints" do 290 | let(:url) { "https://example.com/link_element_absolute_url" } 291 | end 292 | end 293 | 294 | # Similar to https://webmention.rocks/test/9 295 | context "when the `rel` attribute contains multiple space-separated values" do 296 | it_behaves_like "a Hash of endpoints" do 297 | let(:url) { "https://example.com/link_element_multiple_rel_values" } 298 | end 299 | end 300 | 301 | # Similar to https://webmention.rocks/test/12 302 | context "when the `rel` attribute contains similar values" do 303 | it_behaves_like "a Hash of endpoints" do 304 | let(:url) { "https://example.com/link_element_exact_match" } 305 | end 306 | end 307 | 308 | # Similar to https://webmention.rocks/test/13 309 | context "when the HTML contains an endpoint in an HTML comment" do 310 | it_behaves_like "a Hash of endpoints" do 311 | let(:url) { "https://example.com/link_element_html_comment" } 312 | end 313 | end 314 | 315 | # Similar to https://webmention.rocks/test/15 316 | context "when the `href` attribute is empty" do 317 | it_behaves_like "a Hash of endpoints" do 318 | let(:url) { "https://example.com/link_element_empty_href" } 319 | 320 | let(:endpoints) do 321 | { 322 | authorization_endpoint: "https://example.com/link_element_empty_href", 323 | "indieauth-metadata": "https://example.com/link_element_empty_href", 324 | micropub: "https://example.com/link_element_empty_href", 325 | microsub: "https://example.com/link_element_empty_href", 326 | redirect_uri: ["https://example.com/link_element_empty_href"], 327 | token_endpoint: "https://example.com/link_element_empty_href", 328 | webmention: nil, 329 | } 330 | end 331 | end 332 | end 333 | 334 | # Similar to https://webmention.rocks/test/20 335 | context "when the `href` attribute does not exist" do 336 | it_behaves_like "a Hash of endpoints" do 337 | let(:url) { "https://example.com/link_element_no_href" } 338 | end 339 | end 340 | 341 | # Similar to https://webmention.rocks/test/21 342 | context "when `link` element references a URL with a query string" do 343 | it_behaves_like "a Hash of endpoints" do 344 | let(:url) { "https://example.com/link_element_query_string" } 345 | 346 | let(:endpoints) do 347 | { 348 | authorization_endpoint: "https://example.com/authorization_endpoint?query=yes", 349 | "indieauth-metadata": "https://example.com/indieauth-metadata?query=yes", 350 | micropub: "https://example.com/micropub?query=yes", 351 | microsub: "https://example.com/microsub?query=yes", 352 | redirect_uri: ["https://example.com/redirect?query=yes"], 353 | token_endpoint: "https://example.com/token_endpoint?query=yes", 354 | webmention: nil, 355 | } 356 | end 357 | end 358 | end 359 | 360 | # Similar to https://webmention.rocks/test/22 361 | context "when the `link` element references a URL relative to the page" do 362 | it_behaves_like "a Hash of endpoints" do 363 | let(:url) { "https://example.com/link_element/relative_path" } 364 | 365 | let(:endpoints) do 366 | { 367 | authorization_endpoint: "https://example.com/link_element/relative_path/authorization_endpoint", 368 | "indieauth-metadata": "https://example.com/link_element/relative_path/indieauth-metadata", 369 | micropub: "https://example.com/link_element/relative_path/micropub", 370 | microsub: "https://example.com/link_element/relative_path/microsub", 371 | redirect_uri: ["https://example.com/link_element/relative_path/redirect"], 372 | token_endpoint: "https://example.com/link_element/relative_path/token_endpoint", 373 | webmention: nil, 374 | } 375 | end 376 | end 377 | end 378 | 379 | context "when the `link` element references a URL with a fragment" do 380 | it_behaves_like "a Hash of endpoints" do 381 | let(:url) { "https://example.com/link_element_fragment" } 382 | 383 | let(:endpoints) do 384 | { 385 | authorization_endpoint: "https://example.com/authorization_endpoint", 386 | "indieauth-metadata": "https://example.com/indieauth-metadata", 387 | micropub: "https://example.com/micropub", 388 | microsub: "https://example.com/microsub", 389 | redirect_uri: ["https://example.com/redirect_uri"], 390 | token_endpoint: "https://example.com/token_endpoint", 391 | webmention: nil, 392 | } 393 | end 394 | end 395 | end 396 | end 397 | end 398 | --------------------------------------------------------------------------------