indieauth_discovery 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 64aec7aba0f75ed8c80dbfc68dba30a3a321d0275b8d8a991772357eba013caa
4
+ data.tar.gz: 18fee04fbe53329ffa4410a3a4b7712fa0204688423b54ecabe23780668177e8
5
+ SHA512:
6
+ metadata.gz: 289ed40bac4fba8ffec7e968fb0c244f9325978078cff9f2720e4e0f991e3a083bc78c5dc8d98853f50dcbc4e10dcc0b17b3988172c63820d4eeb67f45b92c21
7
+ data.tar.gz: cb523fa2da8aa2263ffc1fd2059acf3a83cf059d49185b593a993c17ad0e687d2c4b3c15ea7963458c993614595667c383c0258bb4c80e6c69fd0e4a1b7d39cd
@@ -0,0 +1,14 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ insert_final_newline = true
7
+
8
+ [{Dangerfile,Gemfile,Rakefile}]
9
+ indent_style = space
10
+ indent_size = 2
11
+
12
+ [*.{json,md,rb,yaml,yml}]
13
+ indent_style = space
14
+ indent_size = 2
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: Bug report
3
+ about: Create a report to help us improve
4
+ title: ''
5
+ labels: bug
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+ **Describe the bug**
11
+ A clear and concise description of what the bug is.
12
+
13
+ **To Reproduce**
14
+ Steps to reproduce the behavior:
15
+ 1. Go to '...'
16
+ 2. Click on '....'
17
+ 3. Scroll down to '....'
18
+ 4. See error
19
+
20
+ **Expected behavior**
21
+ A clear and concise description of what you expected to happen.
22
+
23
+ **Additional context**
24
+ Add any other context about the problem here.
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an idea for this project
4
+ title: ''
5
+ labels: enhancement
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+ **Is your feature request related to a problem? Please describe.**
11
+ A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12
+
13
+ **Describe the solution you'd like**
14
+ A clear and concise description of what you want to happen.
15
+
16
+ **Describe alternatives you've considered**
17
+ A clear and concise description of any alternative solutions or features you've considered.
18
+
19
+ **Additional context**
20
+ Add any other context or screenshots about the feature request here.
@@ -0,0 +1,28 @@
1
+ **Pull Request template**
2
+
3
+ Please, go through these steps before you submit a PR.
4
+
5
+ 1. Make sure that your PR is not a duplicate.
6
+ 2. If not, then make sure that:
7
+
8
+ a. You have done your changes in a separate branch. Branches MUST have descriptive names that start with either the `fix/` or `feature/` prefixes. Good examples are: `fix/some-issue` or `feature/issue-templates`.
9
+
10
+ b. You have a descriptive commit message with a short title (first line).
11
+
12
+ c. You have only one commit (if not, squash them into one commit).
13
+
14
+ d. `bundle exec rspec` doesn't throw any errors. If it does, fix them first and amend your commit (`git commit --amend`).
15
+
16
+ e. `bundle exec rubocop` doesn't throw any errors. If it does, fix them first and amend your commit (`git commit --amend`).
17
+
18
+ 3. **After** these steps, you're ready to open a pull request.
19
+
20
+ a. Give a descriptive title to your PR.
21
+
22
+ b. Provide a description of your changes.
23
+
24
+ c. Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes (if such).
25
+
26
+ IMPORTANT: Please review the [CONTRIBUTING.md](../CONTRIBUTING.md) file for detailed contributing guidelines.
27
+
28
+ **PLEASE REMOVE THIS TEMPLATE BEFORE SUBMITTING**
@@ -0,0 +1,17 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
7
+ commit-message:
8
+ prefix: "chore"
9
+ include: "scope"
10
+
11
+ - package-ecosystem: "bundler"
12
+ directory: "/"
13
+ schedule:
14
+ interval: "daily"
15
+ commit-message:
16
+ prefix: "chore"
17
+ include: "scope"
@@ -0,0 +1,74 @@
1
+ name: Verify
2
+ on: [push, pull_request]
3
+
4
+ jobs:
5
+ lint:
6
+ name: Lint (Ruby ${{ matrix.ruby }})
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ matrix:
10
+ ruby: ['2.5', '2.6', '2.7']
11
+ steps:
12
+ - name: Checkout code
13
+ uses: actions/checkout@v2
14
+
15
+ - name: Set up Ruby
16
+ uses: ruby/setup-ruby@v1
17
+ with:
18
+ ruby-version: ${{ matrix.ruby }}
19
+
20
+ - name: Ruby gem cache
21
+ uses: actions/cache@v2.1.0
22
+ with:
23
+ path: vendor/bundle
24
+ key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
25
+ restore-keys: |
26
+ ${{ runner.os }}-gems-
27
+
28
+ - name: Install gems
29
+ run: |
30
+ bundle config path vendor/bundle
31
+ bundle install --jobs 4 --retry 3
32
+
33
+ - name: Run linters
34
+ run: |
35
+ bundle exec rubocop --parallel
36
+
37
+ - name: Run security checks
38
+ run: |
39
+ bundle exec bundler-audit --update
40
+
41
+ test:
42
+ name: Test (Ruby ${{ matrix.ruby }})
43
+ runs-on: ubuntu-latest
44
+ strategy:
45
+ matrix:
46
+ ruby: ['2.5', '2.6', '2.7']
47
+ steps:
48
+ - name: Checkout code
49
+ uses: actions/checkout@v2
50
+
51
+ - name: Set up Ruby
52
+ uses: ruby/setup-ruby@v1
53
+ with:
54
+ ruby-version: ${{ matrix.ruby }}
55
+
56
+ - name: Ruby gem cache
57
+ uses: actions/cache@v2.1.0
58
+ with:
59
+ path: vendor/bundle
60
+ key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
61
+ restore-keys: |
62
+ ${{ runner.os }}-gems-
63
+
64
+ - name: Install gems
65
+ run: |
66
+ bundle config path vendor/bundle
67
+ bundle install --jobs 4 --retry 3
68
+
69
+ - name: Run tests
70
+ env:
71
+ COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
72
+ RAILS_ENV: test
73
+ run: |
74
+ bundle exec rspec
@@ -0,0 +1,16 @@
1
+ .byebug_history
2
+ .env
3
+ .rspec_status
4
+ .ruby-version
5
+ .rvmrc
6
+ *.gem
7
+ /_yardoc/
8
+ /.bundle/
9
+ /.config
10
+ /.yardoc/
11
+ /coverage/
12
+ /doc/
13
+ /pkg/
14
+ /rdoc/
15
+ /tmp/
16
+ /vendor/bundle
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,22 @@
1
+ #inherit_from: .rubocop_todo.yml
2
+
3
+ require:
4
+ - rubocop-performance
5
+ - rubocop-rspec
6
+
7
+ AllCops:
8
+ NewCops: enable
9
+ Exclude:
10
+ - vendor/**/*
11
+ TargetRubyVersion: 2.5
12
+
13
+ Metrics/BlockLength:
14
+ Exclude:
15
+ - spec/**/*.rb
16
+
17
+ RSpec/ExampleLength:
18
+ Enabled: false
19
+
20
+ RSpec/FilePath:
21
+ CustomTransform:
22
+ IndieAuthDiscovery: indieauth_discovery
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
+
6
+ ## [Unreleased]
7
+
8
+ ## [0.1.0]
9
+
10
+ Initial release! :tada:
11
+
12
+ ### Added
13
+
14
+ * User profile URL and client identifier validation and canonicalization
15
+ * Authorization, token, and MicroPub endpoint discovery from user profiles
16
+
17
+ [Unreleased]: https://github.com/craftyphotons/indieauth_discovery/compare/v0.1.0...HEAD
18
+ [0.1.0]: https://github.com/craftyphotons/indieauth_discovery/releases/tag/v0.1.0
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at tony@tonyburns.net. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [https://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: https://contributor-covenant.org
74
+ [version]: https://contributor-covenant.org/version/1/4/
@@ -0,0 +1,3 @@
1
+ ## Contributing to `indieauth_discovery`
2
+
3
+ Bug reports and pull requests are welcome on GitHub at [craftyphotons/indieauth_discovery](https://github.com/craftyphotons/indieauth_discovery). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/craftyphotons/indieauth_discovery/blob/main/CODE_OF_CONDUCT.md).
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'bundler-audit', '~> 0.7'
8
+ gem 'coveralls', '~> 0.8'
9
+ gem 'rake', '~> 13.0'
10
+ gem 'rspec', '~> 3.9'
11
+ gem 'rubocop', '~> 0.89'
12
+ gem 'rubocop-performance', '~> 1.7'
13
+ gem 'rubocop-rspec', '~> 1.42'
14
+ gem 'webmock', '~> 3.8'
15
+ gem 'yard', '~> 0.9'
@@ -0,0 +1,115 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ indieauth_discovery (0.1.0)
5
+ faraday (~> 1.0)
6
+ faraday_middleware (~> 1.0)
7
+ link-header-parser (~> 2.0)
8
+ nokogiri (~> 1.10)
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ absolutely (4.0.0)
14
+ addressable (~> 2.7)
15
+ addressable (2.7.0)
16
+ public_suffix (>= 2.0.2, < 5.0)
17
+ ast (2.4.1)
18
+ bundler-audit (0.7.0.1)
19
+ bundler (>= 1.2.0, < 3)
20
+ thor (>= 0.18, < 2)
21
+ coveralls (0.8.23)
22
+ json (>= 1.8, < 3)
23
+ simplecov (~> 0.16.1)
24
+ term-ansicolor (~> 1.3)
25
+ thor (>= 0.19.4, < 2.0)
26
+ tins (~> 1.6)
27
+ crack (0.4.3)
28
+ safe_yaml (~> 1.0.0)
29
+ diff-lcs (1.4.4)
30
+ docile (1.3.2)
31
+ faraday (1.0.1)
32
+ multipart-post (>= 1.2, < 3)
33
+ faraday_middleware (1.0.0)
34
+ faraday (~> 1.0)
35
+ hashdiff (1.0.1)
36
+ json (2.3.1)
37
+ link-header-parser (2.0.0)
38
+ absolutely (~> 4.0)
39
+ mini_portile2 (2.4.0)
40
+ multipart-post (2.1.1)
41
+ nokogiri (1.10.10)
42
+ mini_portile2 (~> 2.4.0)
43
+ parallel (1.19.2)
44
+ parser (2.7.1.4)
45
+ ast (~> 2.4.1)
46
+ public_suffix (4.0.5)
47
+ rainbow (3.0.0)
48
+ rake (13.0.1)
49
+ regexp_parser (1.7.1)
50
+ rexml (3.2.4)
51
+ rspec (3.9.0)
52
+ rspec-core (~> 3.9.0)
53
+ rspec-expectations (~> 3.9.0)
54
+ rspec-mocks (~> 3.9.0)
55
+ rspec-core (3.9.2)
56
+ rspec-support (~> 3.9.3)
57
+ rspec-expectations (3.9.2)
58
+ diff-lcs (>= 1.2.0, < 2.0)
59
+ rspec-support (~> 3.9.0)
60
+ rspec-mocks (3.9.1)
61
+ diff-lcs (>= 1.2.0, < 2.0)
62
+ rspec-support (~> 3.9.0)
63
+ rspec-support (3.9.3)
64
+ rubocop (0.89.1)
65
+ parallel (~> 1.10)
66
+ parser (>= 2.7.1.1)
67
+ rainbow (>= 2.2.2, < 4.0)
68
+ regexp_parser (>= 1.7)
69
+ rexml
70
+ rubocop-ast (>= 0.3.0, < 1.0)
71
+ ruby-progressbar (~> 1.7)
72
+ unicode-display_width (>= 1.4.0, < 2.0)
73
+ rubocop-ast (0.3.0)
74
+ parser (>= 2.7.1.4)
75
+ rubocop-performance (1.7.1)
76
+ rubocop (>= 0.82.0)
77
+ rubocop-rspec (1.42.0)
78
+ rubocop (>= 0.87.0)
79
+ ruby-progressbar (1.10.1)
80
+ safe_yaml (1.0.5)
81
+ simplecov (0.16.1)
82
+ docile (~> 1.1)
83
+ json (>= 1.8, < 3)
84
+ simplecov-html (~> 0.10.0)
85
+ simplecov-html (0.10.2)
86
+ sync (0.5.0)
87
+ term-ansicolor (1.7.1)
88
+ tins (~> 1.0)
89
+ thor (1.0.1)
90
+ tins (1.25.0)
91
+ sync
92
+ unicode-display_width (1.7.0)
93
+ webmock (3.8.3)
94
+ addressable (>= 2.3.6)
95
+ crack (>= 0.3.2)
96
+ hashdiff (>= 0.4.0, < 2.0.0)
97
+ yard (0.9.25)
98
+
99
+ PLATFORMS
100
+ ruby
101
+
102
+ DEPENDENCIES
103
+ bundler-audit (~> 0.7)
104
+ coveralls (~> 0.8)
105
+ indieauth_discovery!
106
+ rake (~> 13.0)
107
+ rspec (~> 3.9)
108
+ rubocop (~> 0.89)
109
+ rubocop-performance (~> 1.7)
110
+ rubocop-rspec (~> 1.42)
111
+ webmock (~> 3.8)
112
+ yard (~> 0.9)
113
+
114
+ BUNDLED WITH
115
+ 2.1.4
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Tony Burns
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
13
+ all 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
21
+ THE SOFTWARE.
@@ -0,0 +1,104 @@
1
+ # `indieauth_discovery`
2
+
3
+ [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/craftyphotons/indieauth_discovery/Verify/main?style=for-the-badge)](https://github.com/craftyphotons/indieauth_discovery/actions?query=workflow%3AVerify)
4
+ &nbsp;
5
+ [![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/craftyphotons/indieauth_discovery?style=for-the-badge)](https://codeclimate.com/github/craftyphotons/indieauth_discovery)
6
+ &nbsp;
7
+ [![Coveralls github branch](https://img.shields.io/coveralls/github/craftyphotons/indieauth_discovery/main?style=for-the-badge)](https://coveralls.io/github/craftyphotons/indieauth_discovery)
8
+
9
+ Profile and client discovery for [Ruby](https://www.ruby-lang.org/en)-based [IndieAuth](https://indieauth.spec.indieweb.org) clients and providers.
10
+
11
+ ## Features
12
+
13
+ - [x] [User profile URL](https://indieauth.spec.indieweb.org/#user-profile-url) and [client identifier](https://indieauth.spec.indieweb.org/#client-identifier) validation and [canonicalization](https://indieauth.spec.indieweb.org/#url-canonicalization) with
14
+ - [x] Handling of [permanant and temporary redirects](https://indieauth.spec.indieweb.org/#redirect-examples)
15
+ - [x] [Authorization, token, and MicroPub endpoint discovery](https://indieauth.spec.indieweb.org/#discovery-by-clients) from user profiles
16
+
17
+ ## Roadmap
18
+
19
+ - [ ] [Client information discovery](https://indieauth.spec.indieweb.org/#client-information-discovery) from [`h-app` and `h-xapp`](https://indieweb.org/h-x-app)
20
+ - [ ] [Redirect URI verification](https://indieauth.spec.indieweb.org/#redirect-url)
21
+
22
+ ## Installation
23
+
24
+ Add this line to your application's `Gemfile`:
25
+
26
+ ```ruby
27
+ gem 'indieauth_discovery'
28
+ ```
29
+
30
+ And then execute:
31
+
32
+ $ bundle install
33
+
34
+ Or install it yourself as:
35
+
36
+ $ gem install indieauth_discovery
37
+
38
+ ## Usage
39
+
40
+ ### URL verification and canonicalization
41
+
42
+ `indieauth_discovery` can canonicalize and verify URLs indepedently of information discovery via the `IndieAuthDiscovery::URL` class:
43
+
44
+ ``` ruby
45
+ require 'indieauth_discovery/url'
46
+
47
+ url = IndieAuthDiscovery::URL.new('example.com')
48
+ url.canonicalize
49
+
50
+ # or
51
+
52
+ url = IndieAuthDiscovery::URL.canonicalize('example.com')
53
+
54
+ url.original_url # example.com
55
+ url.canonical_url # http://example.com/
56
+ ```
57
+
58
+ The `#canonicalize` method performs the following steps:
59
+
60
+ 1. Normalizes the URL (downcases the hostname)
61
+ 2. Verifies the URL if already `http` or `https` by performing an HTTP `HEAD` request
62
+ 3. If the URL is generic without a scheme (i.e. `example.com`), attempts to verify the URL with an HTTP `HEAD` request to both `https://<url>` and `http://<url>`, prioritizing HTTPS
63
+ 4. Ensures the URL has a path by appending `/` to it if the path component is empty
64
+ 5. Follows up to three redirects, and uses the last permanent (301) redirect as the canonical URL
65
+
66
+ If none of the steps above result in a verified URL, an `IndieAuthDiscovery::InvalidURLError` will be raised.
67
+
68
+ ### User profile discovery
69
+
70
+ User profile information can be discovered with `indieauth_discovery` via the `IndieAuthDiscovery::Profile` class:
71
+
72
+ ``` ruby
73
+ require 'indieauth_discovery/profile'
74
+
75
+ profile = IndieAuthDiscovery::Profile.new('example.com')
76
+ profile.discover
77
+
78
+ # or
79
+
80
+ profile = IndieAuthDiscovery::Profile.discover('example.com')
81
+
82
+ profile.url # http://example.com/
83
+ profile.authorization_endpoint # http://example.com/auth
84
+ profile.token_endpoint # http://example.com/token
85
+ profile.micropub_endpoint # http://example.com/micropub
86
+ ```
87
+
88
+ ## Development
89
+
90
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
91
+
92
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
93
+
94
+ ## Contributing
95
+
96
+ Bug reports and pull requests are welcome on GitHub at [craftyphotons/indieauth_discovery](https://github.com/craftyphotons/indieauth_discovery). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/craftyphotons/indieauth_discovery/blob/main/CODE_OF_CONDUCT.md).
97
+
98
+ ## License
99
+
100
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
101
+
102
+ ## Code of Conduct
103
+
104
+ Everyone interacting in the IndieauthDiscovery project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/indieauth_discovery/blob/master/CODE_OF_CONDUCT.md).
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'indieauth_discovery'
6
+
7
+ require 'pry'
8
+ Pry.start
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/indieauth_discovery/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'indieauth_discovery'
7
+ spec.version = IndieAuthDiscovery::VERSION
8
+ spec.authors = ['Tony Burns']
9
+ spec.email = ['tony@tonyburns.net']
10
+
11
+ spec.summary = 'IndieAuth profile and client discovery'
12
+ spec.description = 'Profile and client discovery for IndieAuth clients and providers'
13
+ spec.homepage = 'https://github.com/craftyphotons/indieauth_discovery'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = 'https://github.com/craftyphotons/indieauth_discovery'
19
+ spec.metadata['changelog_uri'] = 'https://github.com/craftyphotons/indieauth_discovery/blob/main/CHANGELOG.md'
20
+
21
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^spec/}) }
23
+ end
24
+ spec.bindir = 'exe'
25
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
26
+ spec.require_paths = ['lib']
27
+
28
+ spec.add_runtime_dependency 'faraday', '~> 1.0'
29
+ spec.add_runtime_dependency 'faraday_middleware', '~> 1.0'
30
+ spec.add_runtime_dependency 'link-header-parser', '~> 2.0'
31
+ spec.add_runtime_dependency 'nokogiri', '~> 1.10'
32
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'indieauth_discovery/version'
4
+
5
+ # Provides utilities to discover and verify user profiles and clients for IndieAuth clients and servers.
6
+ module IndieAuthDiscovery
7
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'faraday_middleware/response/follow_redirects'
5
+ require 'link-header-parser'
6
+ require 'nokogiri'
7
+
8
+ require_relative './errors'
9
+ require_relative './url'
10
+
11
+ module IndieAuthDiscovery
12
+ # Client information discovery according to the IndieAuth spec.
13
+ #
14
+ # @see https://indieauth.spec.indieweb.org/#client-information-discovery
15
+ class Profile
16
+ attr_reader :profile_url, :authorization_endpoint, :token_endpoint, :response
17
+
18
+ def initialize(default_scheme: :https)
19
+ @default_scheme = default_scheme
20
+ end
21
+
22
+ def call(url)
23
+ @profile_url = URL.canonicalize(url)
24
+ retrieve
25
+ discover
26
+ end
27
+
28
+ private
29
+
30
+ def retrieve
31
+ @response ||= get_follow_redirects(profile_url)
32
+ @link_headers = parse_link_headers(response)
33
+ @profile_document = parse_html_document(response)
34
+ end
35
+
36
+ def discover
37
+ @authorization_endpoint = first_link('authorization_endpoint')
38
+ @token_endpoint = first_link('token_endpoint')
39
+ end
40
+
41
+ def parse_link_headers(response)
42
+ return {} unless response.headers['link']
43
+
44
+ LinkHeaderParser.parse(response.headers['link'], base: profile_url).group_by_relation_type
45
+ end
46
+
47
+ def parse_html_document(response)
48
+ return Nokogiri::HTML('') unless response.headers['content-type'].start_with?('text/html')
49
+
50
+ Nokogiri::HTML(response.body)
51
+ end
52
+
53
+ def first_link(rel)
54
+ @link_headers[rel.to_sym]&.first&.target_uri ||
55
+ @profile_document.at_xpath("//link[@rel='#{rel}']")&.attribute('href')&.to_s
56
+ end
57
+
58
+ def get_follow_redirects(url)
59
+ Faraday.new(url: url) do |faraday|
60
+ faraday.use(FaradayMiddleware::FollowRedirects)
61
+ faraday.adapter(Faraday.default_adapter)
62
+ end.get
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IndieAuthDiscovery
4
+ # Base class for IndieAuthDiscovery errors.
5
+ class Error < StandardError
6
+ attr_accessor :error, :error_reason, :error_uri
7
+
8
+ def initialize(error, error_reason = nil, error_uri = nil)
9
+ @error = error
10
+ @error_reason = error_reason
11
+ @error_uri = error_uri
12
+
13
+ super(message)
14
+ end
15
+
16
+ def message
17
+ [error, error_reason, error_uri].compact.join(' | ')
18
+ end
19
+ end
20
+
21
+ # Error raised when endpoint discovery fails.
22
+ class DiscoveryError < Error
23
+ end
24
+
25
+ # Error raised when a URL is invalid.
26
+ class InvalidURLError < Error
27
+ end
28
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'faraday_middleware/response/follow_redirects'
5
+ require 'link-header-parser'
6
+ require 'nokogiri'
7
+
8
+ require_relative './errors'
9
+ require_relative './url'
10
+
11
+ module IndieAuthDiscovery
12
+ # User profile information discovery according to the IndieAuth spec.
13
+ #
14
+ # @see https://indieauth.spec.indieweb.org/#discovery-by-clients
15
+ class Profile
16
+ attr_reader :url, :authorization_endpoint, :micropub_endpoint, :token_endpoint, :response
17
+
18
+ def initialize(url)
19
+ @url = URL.new(url)
20
+ end
21
+
22
+ # Returns a new Profile after canonicalizing and verifying the URL and discovering endpoints.
23
+ def self.discover(url)
24
+ new(url).discover
25
+ end
26
+
27
+ # Returns the Profile after canonicalizing and verifying the URL and discovering endpoints.
28
+ def discover
29
+ canonicalize_url
30
+ fetch_profile
31
+ find_endpoints
32
+ self
33
+ end
34
+
35
+ private
36
+
37
+ def canonicalize_url
38
+ url.canonicalize
39
+ end
40
+
41
+ def fetch_profile
42
+ @response ||= get_follow_redirects(url.to_s)
43
+ @link_headers = parse_link_headers(response)
44
+ @profile_document = parse_html_document(response)
45
+ end
46
+
47
+ def find_endpoints
48
+ @authorization_endpoint = first_link('authorization_endpoint')
49
+ @token_endpoint = first_link('token_endpoint')
50
+ @micropub_endpoint = first_link('micropub')
51
+ end
52
+
53
+ def parse_link_headers(response)
54
+ return {} unless response.headers['link']
55
+
56
+ LinkHeaderParser.parse(response.headers['link'], base: url.to_s).group_by_relation_type
57
+ end
58
+
59
+ def parse_html_document(response)
60
+ return Nokogiri::HTML('') unless response.headers['content-type'].start_with?('text/html')
61
+
62
+ Nokogiri::HTML(response.body)
63
+ end
64
+
65
+ def first_link(rel)
66
+ @link_headers[rel.to_sym]&.first&.target_uri ||
67
+ @profile_document.at_xpath("//link[@rel='#{rel}']")&.attribute('href')&.to_s
68
+ end
69
+
70
+ def get_follow_redirects(url)
71
+ Faraday.new(url: url) do |faraday|
72
+ faraday.use(FaradayMiddleware::FollowRedirects)
73
+ faraday.adapter(Faraday.default_adapter)
74
+ end.get
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'faraday_middleware/response/follow_redirects'
5
+ require 'nokogiri'
6
+
7
+ require_relative './errors'
8
+
9
+ module IndieAuthDiscovery
10
+ # Canonicalization for IndieAuth client and user profile URLs.
11
+ class URL
12
+ attr_reader :original_url, :canonical_url
13
+
14
+ def initialize(original_url)
15
+ @original_url = original_url.to_s
16
+ @canonical_url = original_url.to_s
17
+ end
18
+
19
+ def self.canonicalize(original_url)
20
+ url = new(original_url)
21
+ url.canonicalize
22
+ url
23
+ end
24
+
25
+ # Canonicalizes and verifies the URL.
26
+ #
27
+ # @see https://indieauth.spec.indieweb.org/#user-profile-url
28
+ # @see https://indieauth.spec.indieweb.org/#client-identifier
29
+ def canonicalize
30
+ canonical = normalize_url(original_url)
31
+ canonical, verify_response = verify_url(canonical)
32
+ canonical = ensure_path(canonical)
33
+ canonical = follow_redirects(canonical, verify_response)
34
+
35
+ @canonical_url = canonical
36
+ rescue URI::InvalidURIError, *FARADAY_ERRORS
37
+ raise_invalid_url_error(original_url)
38
+ end
39
+
40
+ # Returns the canonical URL as a string.
41
+ def to_s
42
+ @canonical_url
43
+ end
44
+
45
+ # Returns the canonical URL as a URI.
46
+ def to_uri
47
+ URI.parse(@canonical_url)
48
+ end
49
+
50
+ private
51
+
52
+ FARADAY_ERRORS = [Faraday::ConnectionFailed, Faraday::TimeoutError].freeze
53
+
54
+ def normalize_url(url)
55
+ URI.parse(url).normalize
56
+ end
57
+
58
+ # @see https://indieauth.spec.indieweb.org/#url-canonicalization
59
+ def verify_url(uri)
60
+ if http_uri?(uri) && (last_response = check_url?(uri))
61
+ # Use the URL as-is if its already HTTP(S) and is available
62
+ [uri.to_s, last_response]
63
+ elsif (last_response = check_url?(URI.parse("https://#{uri}")))
64
+ # If no scheme was given (e.g. example.com), try HTTPS
65
+ ["https://#{uri}", last_response]
66
+ elsif (last_response = check_url?(URI.parse("http://#{uri}")))
67
+ # Try HTTP if HTTPS is not available
68
+ ["http://#{uri}", last_response]
69
+ else
70
+ # The URL is considered invalid if none of the above work
71
+ raise_invalid_url_error(uri)
72
+ end
73
+ end
74
+
75
+ def http_uri?(uri)
76
+ uri.is_a?(URI::HTTPS) || uri.is_a?(URI::HTTP)
77
+ end
78
+
79
+ def check_url?(uri)
80
+ response = Faraday.head(uri)
81
+ return false unless response.status < 400
82
+
83
+ response
84
+ rescue *FARADAY_ERRORS
85
+ nil
86
+ end
87
+
88
+ def ensure_path(uri)
89
+ return uri unless URI.parse(uri).path == ''
90
+
91
+ "#{uri}/"
92
+ end
93
+
94
+ # @see https://indieauth.spec.indieweb.org/#redirect-examples
95
+ def follow_redirects(uri, response)
96
+ return uri unless [301, 302].include?(response.status)
97
+
98
+ redirects = []
99
+ redirector(uri, redirects).head
100
+
101
+ uri = redirects.last if redirects.any? && redirects.last != uri
102
+ uri
103
+ end
104
+
105
+ def redirector(uri, redirects)
106
+ @redirector ||=
107
+ begin
108
+ callback = ->(old_env, new_env) { redirects << new_env.url.to_s if old_env.status == 301 }
109
+ Faraday.new(url: uri) do |faraday|
110
+ faraday.use(FaradayMiddleware::FollowRedirects, callback: callback)
111
+ faraday.adapter(Faraday.default_adapter)
112
+ end
113
+ end
114
+ end
115
+
116
+ def raise_invalid_url_error(url)
117
+ raise InvalidURLError.new(:invalid_url, 'URL must begin with http:// or https://', url)
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IndieAuthDiscovery
4
+ VERSION = '0.1.0'
5
+ end
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: indieauth_discovery
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tony Burns
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-08-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday_middleware
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: link-header-parser
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: nokogiri
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.10'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.10'
69
+ description: Profile and client discovery for IndieAuth clients and providers
70
+ email:
71
+ - tony@tonyburns.net
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".editorconfig"
77
+ - ".github/ISSUE_TEMPLATE/bug_report.md"
78
+ - ".github/ISSUE_TEMPLATE/feature_request.md"
79
+ - ".github/PULL_REQUEST_TEMPLATE.md"
80
+ - ".github/dependabot.yml"
81
+ - ".github/workflows/verify.yml"
82
+ - ".gitignore"
83
+ - ".rspec"
84
+ - ".rubocop.yml"
85
+ - CHANGELOG.md
86
+ - CODE_OF_CONDUCT.md
87
+ - CONTRIBUTING.md
88
+ - Gemfile
89
+ - Gemfile.lock
90
+ - LICENSE.txt
91
+ - README.md
92
+ - Rakefile
93
+ - bin/console
94
+ - bin/setup
95
+ - indieauth_discovery.gemspec
96
+ - lib/indieauth_discovery.rb
97
+ - lib/indieauth_discovery/client.rb
98
+ - lib/indieauth_discovery/errors.rb
99
+ - lib/indieauth_discovery/profile.rb
100
+ - lib/indieauth_discovery/url.rb
101
+ - lib/indieauth_discovery/version.rb
102
+ homepage: https://github.com/craftyphotons/indieauth_discovery
103
+ licenses:
104
+ - MIT
105
+ metadata:
106
+ homepage_uri: https://github.com/craftyphotons/indieauth_discovery
107
+ source_code_uri: https://github.com/craftyphotons/indieauth_discovery
108
+ changelog_uri: https://github.com/craftyphotons/indieauth_discovery/blob/main/CHANGELOG.md
109
+ post_install_message:
110
+ rdoc_options: []
111
+ require_paths:
112
+ - lib
113
+ required_ruby_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: 2.5.0
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ requirements: []
124
+ rubygems_version: 3.1.2
125
+ signing_key:
126
+ specification_version: 4
127
+ summary: IndieAuth profile and client discovery
128
+ test_files: []