bridgetown-webfinger 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ # A module providing classes to handle Uniform Resource Identifiers ([RFC2396][1])
6
+ #
7
+ # [1]: https://datatracker.ietf.org/doc/html/rfc2396
8
+ module URI
9
+ # A URI for an account on a system
10
+ #
11
+ # @since 0.1.0
12
+ # @api public
13
+ #
14
+ # @example Parsing an acct URI from a string
15
+ #
16
+ # URI.parse("acct:bilbo@bagend.com")
17
+ #
18
+ # See [RFC7565](https://datatracker.ietf.org/doc/html/rfc7565) for more
19
+ # information.
20
+ class Acct < Generic
21
+ # The components of the URI
22
+ #
23
+ # @private
24
+ COMPONENT = [:scheme, :account, :host].freeze
25
+
26
+ # Builds a {URI::Acct} from components
27
+ #
28
+ # Note: Do not enter already percent-encoded data as a component for the
29
+ # account because the implementation will do this step for you. If you're
30
+ # building from components received externally, consider using
31
+ # `URI.decode_uri_component` before using it to build a URI with this
32
+ # method.
33
+ #
34
+ # @since 0.1.0
35
+ # @api public
36
+ #
37
+ # @example Building an acct URI from an array of attributes
38
+ #
39
+ # URI::Acct.build(["bilbo", "bagend.com"])
40
+ #
41
+ # @example Building an acct URI from a hash of attributes
42
+ #
43
+ # URI::Acct.build({ account: "bilbo", host: "bagend.com" })
44
+ #
45
+ # @param args [Hash<Symbol, String>, Array<String>] the components to use,
46
+ # either as a Hash of `{account: String, host: String}` or a positional,
47
+ # 2-element `Array` of the form `[account, host]`
48
+ # @return [URI::Acct] the built URI
49
+ # @raise [ArgumentError] when the components are incorrect
50
+ def self.build(args)
51
+ components = Util.make_components_hash(self, args)
52
+
53
+ components[:account] ||= ""
54
+ components[:account] &&= URI.encode_uri_component(components[:account])
55
+ components[:host] ||= ""
56
+ components[:opaque] = "#{components.delete(:account)}@#{components.delete(:host)}"
57
+
58
+ super(components)
59
+ end
60
+
61
+ # Instantiates a new {URI::Acct} from a long list of components
62
+ #
63
+ # @since 0.1.0
64
+ # @api public
65
+ #
66
+ # @example Manually constructing a URI — with value checking — for the masochistic
67
+ # URI::Acct.new(["acct", nil, nil, nil, nil, nil, "bilbo@bagend.com", nil, nil, nil, true])
68
+ #
69
+ # @param args [Array<String>] the components to set for the URI
70
+ # @return [URI::Acct]
71
+ # @raise [InvalidComponentError] when the account name is malformed
72
+ def initialize(*args)
73
+ super
74
+
75
+ raise InvalidComponentError, "missing opaque part for acct URI" unless @opaque
76
+
77
+ account, _, host = @opaque.rpartition("@")
78
+ @opaque = nil
79
+
80
+ if args[10] # arg_check
81
+ self.account = account
82
+ self.host = host
83
+ else
84
+ @account = account
85
+ @host = host
86
+ end
87
+ end
88
+
89
+ # The account part (also known as the userpart) of the URI
90
+ #
91
+ # @since 0.1.0
92
+ # @api public
93
+ #
94
+ # @example Checking the account for a URI
95
+ #
96
+ # URI.parse("acct:bilbo@bagend.com").account #=> "bilbo"
97
+ #
98
+ # @return [String]
99
+ attr_reader :account
100
+
101
+ # Sets the account part of the URI
102
+ #
103
+ # @since 0.1.0
104
+ # @api public
105
+ #
106
+ # @example Setting the account for a URI
107
+ #
108
+ # uri = URI.parse("acct:bilbo@bagend.com")
109
+ # uri.account = "frodo"
110
+ # uri.account #=> "frodo"
111
+ #
112
+ # @param value [String] a percent-encoded account name for the URI
113
+ # @return [void]
114
+ # @raise [InvalidComponentError] when the value is malformed
115
+ def account=(value)
116
+ raise InvalidComponentError, "missing account part for acct URI" if value.empty?
117
+ raise InvalidComponentError, "account part not percent-encoded for acct URI" if value.include?("@")
118
+
119
+ @account = URI.decode_uri_component(value)
120
+ end
121
+
122
+ # Converts the URI into a string
123
+ #
124
+ # @since 0.1.0
125
+ # @api public
126
+ #
127
+ # @example Converting a URI to a string
128
+ #
129
+ # URI.parse("acct:bilbo@bagend.com").to_s
130
+ #
131
+ # @return [String] the formatted version of the {URI::Acct}
132
+ def to_s
133
+ @scheme + ":" + URI.encode_uri_component(account) + "@" + host
134
+ end
135
+ end
136
+
137
+ register_scheme "ACCT", Acct
138
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ module Webfinger
5
+ # The version of the bridgetown-webfinger gem
6
+ VERSION = "0.1.0"
7
+ end
8
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bridgetown"
4
+ require "zeitwerk"
5
+
6
+ require_relative "bridgetown/webfinger/uri/acct"
7
+
8
+ # A progressive site generator and fullstack framework
9
+ #
10
+ # @see https://www.bridgetownrb.com/
11
+ module Bridgetown
12
+ # A Bridgetown plugin that adds support for serving [Webfinger][1] requests
13
+ #
14
+ # The plugin handles either static or dynamic requests, served with data backed
15
+ # by your Bridgetown site's data.
16
+ #
17
+ # [1]: https://datatracker.ietf.org/doc/html/rfc7033
18
+ module Webfinger
19
+ # The Zeitwerk loader responsible for auto-loading constants
20
+ #
21
+ # @private
22
+ Loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false).tap do |loader|
23
+ loader.ignore(__FILE__)
24
+ loader.ignore(File.join(__dir__, "roda", "plugins", "bridgetown_webfinger"))
25
+ loader.ignore(File.join(__dir__, "bridgetown", "webfinger", "initializer"))
26
+ loader.ignore(File.join(__dir__, "bridgetown", "webfinger", "uri", "acct"))
27
+ loader.inflector.inflect("jrd" => "JRD")
28
+ loader.setup
29
+ end
30
+
31
+ # Checks whether a given string is a URI of a registered type
32
+ #
33
+ # This will not convert the item into a URI object and it is able to
34
+ # properly handle when there are multiple URIs in the string (by returning
35
+ # false).
36
+ #
37
+ # @since 0.1.0
38
+ # @api private
39
+ #
40
+ # @param uri [String] the string to check as a URI
41
+ # @return [Boolean] true when it is a URI, false when it is not
42
+ def self.uri?(uri)
43
+ uri.is_a?(String) &&
44
+ uri == URI.extract(uri).first
45
+ end
46
+ end
47
+ end
48
+
49
+ require_relative "bridgetown/webfinger/initializer"
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A routing tree for building Rack applications
4
+ #
5
+ # @see https://roda.jeremyevans.net/
6
+ class Roda
7
+ # The namespace Roda uses for loading plugins by convention
8
+ module RodaPlugins
9
+ # A plugin that integrates Webfinger behavior into a Bridgetown Roda app
10
+ #
11
+ # This plugin requires the Bridgetown SSR plugin to be enabled before it.
12
+ #
13
+ # It reads data from the `authors` data for the Bridgetown site to validate
14
+ # author accounts, then extracts the `webfinger` key from the author to
15
+ # build the JSON Resource Descriptor.
16
+ module BridgetownWebfinger
17
+ # The Roda hook for configuring the plugin
18
+ #
19
+ # @since 0.1.0
20
+ # @api private
21
+ #
22
+ # @param app [::Roda] the Roda application to configure
23
+ # @return [void]
24
+ def self.configure(app)
25
+ return unless app.opts[:bridgetown_site].nil?
26
+
27
+ # :nocov: Because it's difficult to set up multiple contexts
28
+ raise(
29
+ "Roda app failure: the bridgetown_ssr plugin must be registered before " \
30
+ "bridgetown_webfinger"
31
+ )
32
+ # :nocov:
33
+ end
34
+
35
+ # Methods included in to the Roda request
36
+ #
37
+ # @see http://roda.jeremyevans.net/rdoc/classes/Roda/RodaPlugins/Base/RequestMethods.html
38
+ module RequestMethods
39
+ # Builds the Webfinger route within the Roda application
40
+ #
41
+ # @since 0.1.0
42
+ # @api public
43
+ #
44
+ # @example Enabling the Webfinger route
45
+ #
46
+ # class RodaApp < Bridgetown::Rack::Roda
47
+ # plugin :bridgetown_ssr
48
+ # plugin :bridgetown_webfinger
49
+ #
50
+ # route do |r|
51
+ # r.bridgetown_webfinger
52
+ # end
53
+ # end
54
+ #
55
+ # @return [void]
56
+ def bridgetown_webfinger
57
+ bridgetown_site = roda_class.opts[:bridgetown_site]
58
+
59
+ on(".well-known/webfinger") do
60
+ is do
61
+ get do
62
+ params = Bridgetown::Webfinger::Parameters.from_query_string(query_string)
63
+ host = URI(bridgetown_site.config.url).host
64
+ response["Access-Control-Allow-Origin"] =
65
+ bridgetown_site.config.webfinger.allowed_origins
66
+
67
+ unless (resource = params.resource)
68
+ next response.webfinger_error("Missing required parameter: resource", status: 400)
69
+ end
70
+
71
+ if (jrd = webfinger_maybe_jrd(resource, host: host, params: params, site: bridgetown_site))
72
+ response.webfinger_success(jrd)
73
+ else
74
+ response.webfinger_error("Unknown resource: #{resource}", status: 404)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ # Constructs a JRD for the appropriate account from author metadata
84
+ #
85
+ # @since 0.1.0
86
+ # @api private
87
+ #
88
+ # @param resource [String] the resource URI from the parameters
89
+ # @param host [String] the host domain from the site configuration
90
+ # @param params [Bridgetown::Webfinger::Parameters] the parameters for the request
91
+ # @param site [Bridgetown::Site] the site for the Bridgetown instance
92
+ # @return [Bridgetown::Webfinger::JRD, nil] the JRD when possible, nil otherwise
93
+ def webfinger_maybe_jrd(resource, host:, params:, site:)
94
+ return unless (uri = URI.parse(resource)).instance_of?(URI::Acct)
95
+ return unless (authors = site.data.authors)
96
+
97
+ webfinger =
98
+ if (acct = authors[uri.account])
99
+ acct.webfinger
100
+ elsif uri.account.empty? && uri.host == host && authors.length == 1
101
+ uri.account = uri.host
102
+ authors.values.first.webfinger
103
+ else
104
+ return
105
+ end
106
+
107
+ jrd = Bridgetown::Webfinger::JRD.parse(uri.to_s, webfinger)
108
+ jrd.links&.select! { |link| params.rel.include?(link.rel) } if params.rel
109
+ jrd
110
+ end
111
+ end
112
+
113
+ # Methods included in to the Roda response
114
+ #
115
+ # @see http://roda.jeremyevans.net/rdoc/classes/Roda/RodaPlugins/Base/ResponseMethods.html
116
+ module ResponseMethods
117
+ # Renders an error in the style of Bridgetown Webfinger
118
+ #
119
+ # @since 0.1.0
120
+ # @api private
121
+ #
122
+ # @param message [String] the error message for the response
123
+ # @param status [Integer] the status code for the response
124
+ # @return [String] the response body
125
+ def webfinger_error(message, status:)
126
+ self["Content-Type"] = "application/json"
127
+ self.status = status
128
+
129
+ JSON.pretty_generate({error: message})
130
+ end
131
+
132
+ # Renders a JSON Resource Descriptor for Webfinger
133
+ #
134
+ # @since 0.1.0
135
+ # @api private
136
+ #
137
+ # @param jrd [Bridgetown::Webfinger::JRD] the JRD to render
138
+ # @return [String] the response body
139
+ def webfinger_success(jrd)
140
+ self["Content-Type"] = "application/jrd+json"
141
+
142
+ JSON.pretty_generate(jrd.to_h)
143
+ end
144
+ end
145
+ end
146
+
147
+ register_plugin :bridgetown_webfinger, BridgetownWebfinger
148
+ end
149
+ end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bridgetown-webfinger
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Michael Herold
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-02-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bridgetown
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '1.2'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '2.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '1.2'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: uri
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 0.12.0
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 0.12.0
47
+ - !ruby/object:Gem::Dependency
48
+ name: zeitwerk
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: bundler
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '2'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '2'
75
+ description:
76
+ email: opensource@michaeljherold.com
77
+ executables: []
78
+ extensions: []
79
+ extra_rdoc_files: []
80
+ files:
81
+ - CHANGELOG.md
82
+ - CONTRIBUTING.md
83
+ - LICENSE.md
84
+ - README.md
85
+ - bridgetown-webfinger.gemspec
86
+ - lib/bridgetown-webfinger.rb
87
+ - lib/bridgetown/webfinger/alias.rb
88
+ - lib/bridgetown/webfinger/builder.rb
89
+ - lib/bridgetown/webfinger/href.rb
90
+ - lib/bridgetown/webfinger/initializer.rb
91
+ - lib/bridgetown/webfinger/jrd.rb
92
+ - lib/bridgetown/webfinger/link.rb
93
+ - lib/bridgetown/webfinger/link_relation_type.rb
94
+ - lib/bridgetown/webfinger/logging.rb
95
+ - lib/bridgetown/webfinger/model.rb
96
+ - lib/bridgetown/webfinger/parameters.rb
97
+ - lib/bridgetown/webfinger/properties.rb
98
+ - lib/bridgetown/webfinger/titles.rb
99
+ - lib/bridgetown/webfinger/uri/acct.rb
100
+ - lib/bridgetown/webfinger/version.rb
101
+ - lib/roda/plugins/bridgetown_webfinger.rb
102
+ homepage: https://github.com/michaelherold/bridgetown-webfinger
103
+ licenses:
104
+ - MIT
105
+ metadata:
106
+ bug_tracker_uri: https://github.com/michaelherold/bridgetown-webfinger/issues
107
+ changelog_uri: https://github.com/michaelherold/bridgetown-webfinger/blob/main/CHANGELOG.md
108
+ documentation_uri: https://rubydoc.info/gems/bridgetown-webfinger/0.1.0
109
+ homepage_uri: https://github.com/michaelherold/bridgetown-webfinger
110
+ rubygems_mfa_required: 'true'
111
+ source_code_uri: https://github.com/michaelherold/bridgetown-webfinger
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: 3.0.0
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubygems_version: 3.2.32
128
+ signing_key:
129
+ specification_version: 4
130
+ summary: Adds structured support for Webfinger to Bridgetown sites
131
+ test_files: []