ruby-openid2 3.0.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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +136 -0
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/CONTRIBUTING.md +54 -0
  6. data/LICENSE.txt +210 -0
  7. data/README.md +81 -0
  8. data/SECURITY.md +15 -0
  9. data/lib/hmac/hmac.rb +110 -0
  10. data/lib/hmac/sha1.rb +11 -0
  11. data/lib/hmac/sha2.rb +25 -0
  12. data/lib/openid/association.rb +246 -0
  13. data/lib/openid/consumer/associationmanager.rb +354 -0
  14. data/lib/openid/consumer/checkid_request.rb +179 -0
  15. data/lib/openid/consumer/discovery.rb +516 -0
  16. data/lib/openid/consumer/discovery_manager.rb +144 -0
  17. data/lib/openid/consumer/html_parse.rb +142 -0
  18. data/lib/openid/consumer/idres.rb +513 -0
  19. data/lib/openid/consumer/responses.rb +147 -0
  20. data/lib/openid/consumer/session.rb +36 -0
  21. data/lib/openid/consumer.rb +406 -0
  22. data/lib/openid/cryptutil.rb +112 -0
  23. data/lib/openid/dh.rb +84 -0
  24. data/lib/openid/extension.rb +38 -0
  25. data/lib/openid/extensions/ax.rb +552 -0
  26. data/lib/openid/extensions/oauth.rb +88 -0
  27. data/lib/openid/extensions/pape.rb +170 -0
  28. data/lib/openid/extensions/sreg.rb +268 -0
  29. data/lib/openid/extensions/ui.rb +49 -0
  30. data/lib/openid/fetchers.rb +277 -0
  31. data/lib/openid/kvform.rb +113 -0
  32. data/lib/openid/kvpost.rb +62 -0
  33. data/lib/openid/message.rb +555 -0
  34. data/lib/openid/protocolerror.rb +7 -0
  35. data/lib/openid/server.rb +1571 -0
  36. data/lib/openid/store/filesystem.rb +260 -0
  37. data/lib/openid/store/interface.rb +73 -0
  38. data/lib/openid/store/memcache.rb +109 -0
  39. data/lib/openid/store/memory.rb +79 -0
  40. data/lib/openid/store/nonce.rb +72 -0
  41. data/lib/openid/trustroot.rb +597 -0
  42. data/lib/openid/urinorm.rb +72 -0
  43. data/lib/openid/util.rb +119 -0
  44. data/lib/openid/version.rb +5 -0
  45. data/lib/openid/yadis/accept.rb +141 -0
  46. data/lib/openid/yadis/constants.rb +16 -0
  47. data/lib/openid/yadis/discovery.rb +151 -0
  48. data/lib/openid/yadis/filters.rb +192 -0
  49. data/lib/openid/yadis/htmltokenizer.rb +290 -0
  50. data/lib/openid/yadis/parsehtml.rb +50 -0
  51. data/lib/openid/yadis/services.rb +44 -0
  52. data/lib/openid/yadis/xrds.rb +160 -0
  53. data/lib/openid/yadis/xri.rb +86 -0
  54. data/lib/openid/yadis/xrires.rb +87 -0
  55. data/lib/openid.rb +27 -0
  56. data/lib/ruby-openid.rb +1 -0
  57. data.tar.gz.sig +0 -0
  58. metadata +331 -0
  59. metadata.gz.sig +0 -0
@@ -0,0 +1,290 @@
1
+ # = HTMLTokenizer
2
+ #
3
+ # Author:: Ben Giddings (mailto:bg-rubyforge@infofiend.com)
4
+ # Copyright:: Copyright (c) 2004 Ben Giddings
5
+ # License:: Distributes under the same terms as Ruby
6
+ #
7
+ #
8
+ # This is a partial port of the functionality behind Perl's TokeParser
9
+ # Provided a page it progressively returns tokens from that page
10
+ #
11
+ # $Id: htmltokenizer.rb,v 1.7 2005/06/07 21:05:53 merc Exp $
12
+
13
+ #
14
+ # A class to tokenize HTML.
15
+ #
16
+ # Example:
17
+ #
18
+ # page = "<HTML>
19
+ # <HEAD>
20
+ # <TITLE>This is the title</TITLE>
21
+ # </HEAD>
22
+ # <!-- Here comes the <a href=\"missing.link\">blah</a>
23
+ # comment body
24
+ # -->
25
+ # <BODY>
26
+ # <H1>This is the header</H1>
27
+ # <P>
28
+ # This is the paragraph, it contains
29
+ # <a href=\"link.html\">links</a>,
30
+ # <img src=\"blah.gif\" optional alt='images
31
+ # are
32
+ # really cool'>. Ok, here is some more text and
33
+ # <A href=\"http://another.link.com/\" target=\"_blank\">another link</A>.
34
+ # </P>
35
+ # </body>
36
+ # </HTML>
37
+ # "
38
+ # toke = HTMLTokenizer.new(page)
39
+ #
40
+ # assert("<h1>" == toke.getTag("h1", "h2", "h3").to_s.downcase)
41
+ # assert(HTMLTag.new("<a href=\"link.html\">") == toke.getTag("IMG", "A"))
42
+ # assert("links" == toke.getTrimmedText)
43
+ # assert(toke.getTag("IMG", "A").attr_hash['optional'])
44
+ # assert("_blank" == toke.getTag("IMG", "A").attr_hash['target'])
45
+ #
46
+ class HTMLTokenizer
47
+ @@version = 1.0
48
+
49
+ # Get version of HTMLTokenizer lib
50
+ def self.version
51
+ @@version
52
+ end
53
+
54
+ attr_reader :page
55
+
56
+ # Create a new tokenizer, based on the content, used as a string.
57
+ def initialize(content)
58
+ @page = content.to_s
59
+ @cur_pos = 0
60
+ end
61
+
62
+ # Reset the parser, setting the current position back at the stop
63
+ def reset
64
+ @cur_pos = 0
65
+ end
66
+
67
+ # Look at the next token, but don't actually grab it
68
+ def peekNextToken
69
+ return if @cur_pos == @page.length
70
+
71
+ if "<" == @page[@cur_pos]
72
+ # Next token is a tag of some kind
73
+ if "!--" == @page[(@cur_pos + 1), 3]
74
+ # Token is a comment
75
+ tag_end = @page.index("-->", (@cur_pos + 1))
76
+ raise HTMLTokenizerError, "No end found to started comment:\n#{@page[@cur_pos, 80]}" if tag_end.nil?
77
+
78
+ # p @page[@cur_pos .. (tag_end+2)]
79
+ HTMLComment.new(@page[@cur_pos..(tag_end + 2)])
80
+ else
81
+ # Token is a html tag
82
+ tag_end = @page.index(">", (@cur_pos + 1))
83
+ raise HTMLTokenizerError, "No end found to started tag:\n#{@page[@cur_pos, 80]}" if tag_end.nil?
84
+
85
+ # p @page[@cur_pos .. tag_end]
86
+ HTMLTag.new(@page[@cur_pos..tag_end])
87
+ end
88
+ else
89
+ # Next token is text
90
+ text_end = @page.index("<", @cur_pos)
91
+ text_end = text_end.nil? ? -1 : (text_end - 1)
92
+ # p @page[@cur_pos .. text_end]
93
+ HTMLText.new(@page[@cur_pos..text_end])
94
+ end
95
+ end
96
+
97
+ # Get the next token, returns an instance of
98
+ # * HTMLText
99
+ # * HTMLToken
100
+ # * HTMLTag
101
+ def getNextToken
102
+ token = peekNextToken
103
+ if token
104
+ # @page = @page[token.raw.length .. -1]
105
+ # @page.slice!(0, token.raw.length)
106
+ @cur_pos += token.raw.length
107
+ end
108
+ # p token
109
+ # print token.raw
110
+ token
111
+ end
112
+
113
+ # Get a tag from the specified set of desired tags.
114
+ # For example:
115
+ # <tt>foo = toke.getTag("h1", "h2", "h3")</tt>
116
+ # Will return the next header tag encountered.
117
+ def getTag(*sought_tags)
118
+ sought_tags.collect! { |elm| elm.downcase }
119
+
120
+ while (tag = getNextToken)
121
+ if tag.is_a?(HTMLTag) and
122
+ (0 == sought_tags.length or sought_tags.include?(tag.tag_name))
123
+ break
124
+ end
125
+ end
126
+ tag
127
+ end
128
+
129
+ # Get all the text between the current position and the next tag
130
+ # (if specified) or a specific later tag
131
+ def getText(until_tag = nil)
132
+ if until_tag.nil?
133
+ if "<" == @page[@cur_pos]
134
+ # Next token is a tag, not text
135
+ ""
136
+ else
137
+ # Next token is text
138
+ getNextToken.text
139
+ end
140
+ else
141
+ ret_str = ""
142
+
143
+ while (tag = peekNextToken)
144
+ break if tag.is_a?(HTMLTag) and tag.tag_name == until_tag
145
+
146
+ ret_str << (tag.text + " ") if "" != tag.text
147
+ getNextToken
148
+ end
149
+
150
+ ret_str
151
+ end
152
+ end
153
+
154
+ # Like getText, but squeeze all whitespace, getting rid of
155
+ # leading and trailing whitespace, and squeezing multiple
156
+ # spaces into a single space.
157
+ def getTrimmedText(until_tag = nil)
158
+ getText(until_tag).strip.gsub(/\s+/m, " ")
159
+ end
160
+ end
161
+
162
+ class HTMLTokenizerError < Exception
163
+ end
164
+
165
+ # The parent class for all three types of HTML tokens
166
+ class HTMLToken
167
+ attr_accessor :raw
168
+
169
+ # Initialize the token based on the raw text
170
+ def initialize(text)
171
+ @raw = text
172
+ end
173
+
174
+ # By default, return exactly the string used to create the text
175
+ def to_s
176
+ raw
177
+ end
178
+
179
+ # By default tokens have no text representation
180
+ def text
181
+ ""
182
+ end
183
+
184
+ def trimmed_text
185
+ text.strip.gsub(/\s+/m, " ")
186
+ end
187
+
188
+ # Compare to another based on the raw source
189
+ def ==(other)
190
+ raw == other.to_s
191
+ end
192
+ end
193
+
194
+ # Class representing text that isn't inside a tag
195
+ class HTMLText < HTMLToken
196
+ def text
197
+ raw
198
+ end
199
+ end
200
+
201
+ # Class representing an HTML comment
202
+ class HTMLComment < HTMLToken
203
+ attr_accessor :contents
204
+
205
+ def initialize(text)
206
+ super
207
+ temp_arr = text.scan(/^<!--\s*(.*?)\s*-->$/m)
208
+ raise HTMLTokenizerError, "Text passed to HTMLComment.initialize is not a comment" if temp_arr[0].nil?
209
+
210
+ @contents = temp_arr[0][0]
211
+ end
212
+ end
213
+
214
+ # Class representing an HTML tag
215
+ class HTMLTag < HTMLToken
216
+ attr_reader :end_tag, :tag_name
217
+
218
+ def initialize(text)
219
+ super
220
+ if "<" != text[0] or ">" != text[-1]
221
+ raise HTMLTokenizerError, "Text passed to HTMLComment.initialize is not a comment"
222
+ end
223
+
224
+ @attr_hash = {}
225
+ @raw = text
226
+
227
+ tag_name = text.scan(/[\w:-]+/)[0]
228
+ raise HTMLTokenizerError, "Error, tag is nil: #{tag_name}" if tag_name.nil?
229
+
230
+ if "/" == text[1]
231
+ # It's an end tag
232
+ @end_tag = true
233
+ @tag_name = "/" + tag_name.downcase
234
+ else
235
+ @end_tag = false
236
+ @tag_name = tag_name.downcase
237
+ end
238
+
239
+ @hashed = false
240
+ end
241
+
242
+ # Retrieve a hash of all the tag's attributes.
243
+ # Lazily done, so that if you don't look at a tag's attributes
244
+ # things go quicker
245
+ def attr_hash
246
+ # Lazy initialize == don't build the hash until it's needed
247
+ unless @hashed
248
+ unless @end_tag
249
+ # Get the attributes
250
+ attr_arr = @raw.scan(%r{<[\w:-]+\s+(.*?)/?>}m)[0]
251
+ if attr_arr.is_a?(Array)
252
+ # Attributes found, parse them
253
+ attrs = attr_arr[0]
254
+ attr_arr = attrs.scan(/\s*([\w:-]+)(?:\s*=\s*("[^"]*"|'[^']*'|([^"'>][^\s>]*)))?/m)
255
+ # clean up the array by:
256
+ # * setting all nil elements to true
257
+ # * removing enclosing quotes
258
+ attr_arr.each do |item|
259
+ val = if item[1].nil?
260
+ item[0]
261
+ elsif '"'[0] == item[1][0] or "'"[0] == item[1][0]
262
+ item[1][1..-2]
263
+ else
264
+ item[1]
265
+ end
266
+ @attr_hash[item[0].downcase] = val
267
+ end
268
+ end
269
+ end
270
+ @hashed = true
271
+ end
272
+
273
+ # p self
274
+
275
+ @attr_hash
276
+ end
277
+
278
+ # Get the 'alt' text for a tag, if it exists, or an empty string otherwise
279
+ def text
280
+ unless end_tag
281
+ case tag_name
282
+ when "img"
283
+ return attr_hash["alt"] unless attr_hash["alt"].nil?
284
+ when "applet"
285
+ return attr_hash["alt"] unless attr_hash["alt"].nil?
286
+ end
287
+ end
288
+ ""
289
+ end
290
+ end
@@ -0,0 +1,50 @@
1
+ # stdlib
2
+ require "cgi"
3
+
4
+ # This library
5
+ require_relative "htmltokenizer"
6
+
7
+ module OpenID
8
+ module Yadis
9
+ def self.html_yadis_location(html)
10
+ parser = HTMLTokenizer.new(html)
11
+
12
+ # to keep track of whether or not we are in the head element
13
+ in_head = false
14
+
15
+ begin
16
+ while el = parser.getTag(
17
+ "head",
18
+ "/head",
19
+ "meta",
20
+ "body",
21
+ "/body",
22
+ "html",
23
+ "script",
24
+ )
25
+
26
+ # we are leaving head or have reached body, so we bail
27
+ return if ["/head", "body", "/body"].member?(el.tag_name)
28
+
29
+ if el.tag_name == "head" && !(el.to_s[-2] == "/")
30
+ in_head = true # tag ends with a /: a short tag
31
+ end
32
+ next unless in_head
33
+
34
+ if el.tag_name == "script" && !(el.to_s[-2] == "/")
35
+ parser.getTag("/script") # tag ends with a /: a short tag
36
+ end
37
+
38
+ return if el.tag_name == "html"
39
+
40
+ next unless el.tag_name == "meta" and (equiv = el.attr_hash["http-equiv"])
41
+ if %w[x-xrds-location x-yadis-location].member?(equiv.downcase) &&
42
+ el.attr_hash.member?("content")
43
+ return CGI.unescapeHTML(el.attr_hash["content"])
44
+ end
45
+ end
46
+ rescue HTMLTokenizerError # just stop parsing if there's an error
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,44 @@
1
+ require_relative "filters"
2
+ require_relative "discovery"
3
+ require_relative "xrds"
4
+
5
+ module OpenID
6
+ module Yadis
7
+ def self.get_service_endpoints(input_url, flt = nil)
8
+ # Perform the Yadis protocol on the input URL and return an
9
+ # iterable of resulting endpoint objects.
10
+ #
11
+ # @param flt: A filter object or something that is convertable
12
+ # to a filter object (using mkFilter) that will be used to
13
+ # generate endpoint objects. This defaults to generating
14
+ # BasicEndpoint objects.
15
+ result = Yadis.discover(input_url)
16
+ begin
17
+ endpoints = Yadis.apply_filter(
18
+ result.normalized_uri,
19
+ result.response_text,
20
+ flt,
21
+ )
22
+ rescue XRDSError => e
23
+ raise DiscoveryFailure.new(e.to_s, nil)
24
+ end
25
+
26
+ [result.normalized_uri, endpoints]
27
+ end
28
+
29
+ def self.apply_filter(normalized_uri, xrd_data, flt = nil)
30
+ # Generate an iterable of endpoint objects given this input data,
31
+ # presumably from the result of performing the Yadis protocol.
32
+
33
+ flt = Yadis.make_filter(flt)
34
+ et = Yadis.parseXRDS(xrd_data)
35
+
36
+ endpoints = []
37
+ each_service(et) do |service_element|
38
+ endpoints += flt.get_service_endpoints(normalized_uri, service_element)
39
+ end
40
+
41
+ endpoints
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,160 @@
1
+ require "rexml/document"
2
+ require "rexml/element"
3
+ require "rexml/xpath"
4
+
5
+ require_relative "xri"
6
+
7
+ module OpenID
8
+ module Yadis
9
+ XRD_NS_2_0 = "xri://$xrd*($v*2.0)"
10
+ XRDS_NS = "xri://$xrds"
11
+
12
+ XRDS_NAMESPACES = {
13
+ "xrds" => XRDS_NS,
14
+ "xrd" => XRD_NS_2_0,
15
+ }
16
+
17
+ class XRDSError < StandardError; end
18
+
19
+ # Raised when there's an assertion in the XRDS that it does not
20
+ # have the authority to make.
21
+ class XRDSFraud < XRDSError
22
+ end
23
+
24
+ def self.get_canonical_id(iname, xrd_tree)
25
+ # Return the CanonicalID from this XRDS document.
26
+ #
27
+ # @param iname: the XRI being resolved.
28
+ # @type iname: unicode
29
+ #
30
+ # @param xrd_tree: The XRDS output from the resolver.
31
+ #
32
+ # @returns: The XRI CanonicalID or None.
33
+ # @returntype: unicode or None
34
+
35
+ xrd_list = []
36
+ REXML::XPath.match(xrd_tree.root, "/xrds:XRDS/xrd:XRD", XRDS_NAMESPACES).each do |el|
37
+ xrd_list << el
38
+ end
39
+
40
+ xrd_list.reverse!
41
+
42
+ cid_elements = []
43
+
44
+ unless xrd_list.empty?
45
+ xrd_list[0].elements.each do |e|
46
+ next unless e.respond_to?(:name)
47
+
48
+ cid_elements << e if e.name == "CanonicalID"
49
+ end
50
+ end
51
+
52
+ cid_element = cid_elements[0]
53
+
54
+ return unless cid_element
55
+
56
+ canonical_id = XRI.make_xri(cid_element.text)
57
+
58
+ child_id = canonical_id.downcase
59
+
60
+ xrd_list[1..-1].each do |xrd|
61
+ parent_sought = child_id[0...child_id.rindex("!")]
62
+
63
+ parent = XRI.make_xri(xrd.elements["CanonicalID"].text)
64
+
65
+ if parent_sought != parent.downcase
66
+ raise XRDSFraud.new(format(
67
+ "%s can not come from %s",
68
+ parent_sought,
69
+ parent,
70
+ ))
71
+ end
72
+
73
+ child_id = parent_sought
74
+ end
75
+
76
+ root = XRI.root_authority(iname)
77
+ unless XRI.provider_is_authoritative(root, child_id)
78
+ raise XRDSFraud.new(format("%s can not come from root %s", child_id, root))
79
+ end
80
+
81
+ canonical_id
82
+ end
83
+
84
+ class XRDSError < StandardError
85
+ end
86
+
87
+ def self.parseXRDS(text)
88
+ disable_entity_expansion do
89
+ raise XRDSError.new("Not an XRDS document.") if text.nil?
90
+
91
+ begin
92
+ d = REXML::Document.new(text)
93
+ rescue RuntimeError
94
+ raise XRDSError.new("Not an XRDS document. Failed to parse XML.")
95
+ end
96
+
97
+ return d if is_xrds?(d)
98
+
99
+ raise XRDSError.new("Not an XRDS document.")
100
+ end
101
+ end
102
+
103
+ def self.disable_entity_expansion
104
+ _previous_ = REXML::Document.entity_expansion_limit
105
+ REXML::Document.entity_expansion_limit = 0
106
+ yield
107
+ ensure
108
+ REXML::Document.entity_expansion_limit = _previous_
109
+ end
110
+
111
+ def self.is_xrds?(xrds_tree)
112
+ xrds_root = xrds_tree.root
113
+ (!xrds_root.nil? and
114
+ xrds_root.name == "XRDS" and
115
+ xrds_root.namespace == XRDS_NS)
116
+ end
117
+
118
+ def self.get_yadis_xrd(xrds_tree)
119
+ REXML::XPath.each(
120
+ xrds_tree.root,
121
+ "/xrds:XRDS/xrd:XRD[last()]",
122
+ XRDS_NAMESPACES,
123
+ ) do |el|
124
+ return el
125
+ end
126
+ raise XRDSError.new("No XRD element found.")
127
+ end
128
+
129
+ # aka iterServices in Python
130
+ def self.each_service(xrds_tree, &block)
131
+ xrd = get_yadis_xrd(xrds_tree)
132
+ xrd.each_element("Service", &block)
133
+ end
134
+
135
+ def self.services(xrds_tree)
136
+ s = []
137
+ each_service(xrds_tree) do |service|
138
+ s << service
139
+ end
140
+ s
141
+ end
142
+
143
+ def self.expand_service(service_element)
144
+ es = service_element.elements
145
+ uris = es.each("URI") { |u| }
146
+ uris = prio_sort(uris)
147
+ types = es.each("Type/text()")
148
+ # REXML::Text objects are not strings.
149
+ types = types.collect { |t| t.to_s }
150
+ uris.collect { |uri| [types, uri.text, service_element] }
151
+ end
152
+
153
+ # Sort a list of elements that have priority attributes.
154
+ def self.prio_sort(elements)
155
+ elements.sort do |a, b|
156
+ a.attribute("priority").to_s.to_i <=> b.attribute("priority").to_s.to_i
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,86 @@
1
+ require_relative "../fetchers"
2
+
3
+ module OpenID
4
+ module Yadis
5
+ module XRI
6
+ # The '(' is for cross-reference authorities, and hopefully has a
7
+ # matching ')' somewhere.
8
+ XRI_AUTHORITIES = ["!", "=", "@", "+", "$", "("]
9
+
10
+ def self.identifier_scheme(identifier)
11
+ if !identifier.nil? and
12
+ identifier.length > 0 and
13
+ (identifier.match("^xri://") or
14
+ XRI_AUTHORITIES.member?(identifier[0].chr))
15
+ :xri
16
+ else
17
+ :uri
18
+ end
19
+ end
20
+
21
+ # Transform an XRI reference to an IRI reference. Note this is
22
+ # not not idempotent, so do not apply this to an identifier more
23
+ # than once. XRI Syntax section 2.3.1
24
+ def self.to_iri_normal(xri)
25
+ iri = xri.dup
26
+ iri.insert(0, "xri://") unless iri.match?("^xri://")
27
+ escape_for_iri(iri)
28
+ end
29
+
30
+ # Note this is not not idempotent, so do not apply this more than
31
+ # once. XRI Syntax section 2.3.2
32
+ def self.escape_for_iri(xri)
33
+ esc = xri.dup
34
+ # encode all %
35
+ esc.gsub!("%", "%25")
36
+ esc.gsub!(/\((.*?)\)/) do |xref_match|
37
+ xref_match.gsub(%r{[/?\#]}) do |char_match|
38
+ CGI.escape(char_match)
39
+ end
40
+ end
41
+ esc
42
+ end
43
+
44
+ # Transform an XRI reference to a URI reference. Note this is not
45
+ # not idempotent, so do not apply this to an identifier more than
46
+ # once. XRI Syntax section 2.3.1
47
+ def self.to_uri_normal(xri)
48
+ iri_to_uri(to_iri_normal(xri))
49
+ end
50
+
51
+ # RFC 3987 section 3.1
52
+ def self.iri_to_uri(iri)
53
+ iri.dup
54
+ # for char in ucschar or iprivate
55
+ # convert each char to %HH%HH%HH (as many %HH as octets)
56
+ end
57
+
58
+ def self.provider_is_authoritative(provider_id, canonical_id)
59
+ lastbang = canonical_id.rindex("!")
60
+ return false unless lastbang
61
+
62
+ parent = canonical_id[0...lastbang]
63
+ parent == provider_id
64
+ end
65
+
66
+ def self.root_authority(xri)
67
+ xri = xri[6..-1] if xri.index("xri://") == 0
68
+ authority = xri.split("/", 2)[0]
69
+ root = if authority[0].chr == "("
70
+ authority[0...authority.index(")") + 1]
71
+ elsif XRI_AUTHORITIES.member?(authority[0].chr)
72
+ authority[0].chr
73
+ else
74
+ authority.split(/[!*]/)[0]
75
+ end
76
+
77
+ make_xri(root)
78
+ end
79
+
80
+ def self.make_xri(xri)
81
+ xri = "xri://" + xri if xri.index("xri://") != 0
82
+ xri
83
+ end
84
+ end
85
+ end
86
+ end