ruby-openid2 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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