barx 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,1056 @@
1
+ module XriResolver
2
+ XRDNS = {"xrd"=>"xri://$xrd*($v*2.0)"} # XRD xml namespace
3
+ XRDType = "xri://$res*auth*($v*2.0)"
4
+
5
+ class XRIRoot
6
+ class <<self
7
+ def authorities
8
+ { "@"=>{:authority_uris=>["https://at.xri.net/", "http://at.xri.net/"], :verify_server_cert=>true},
9
+ "="=>{:authority_uris=>["https://equal.xri.net/", "http://equal.xri.net/"], :verify_server_cert=>true}}
10
+ end
11
+ end
12
+ end
13
+
14
+ class AuthorityResolver
15
+ ## usage:
16
+ ## resolved = XriResolver::AuthorityResolver.resolve(xri, req, stack)
17
+ ##
18
+ ## where xri is an XriParser::AuthorityParser object or an XRI string,
19
+ ## req is an XriResolver::ParseInputParameters object.
20
+ ## stack is an array of nested Ref(s) or Redirect(s) used for cycle detection
21
+ ##
22
+ ## Returns an XriResolver::AuthorityResolver object.
23
+ class <<self
24
+ def resolve(xri, req_obj=nil, stack=nil)
25
+ ## create default ParseInputParameters object unless one is passed in
26
+ req = (XriResolver::ParseInputParameters === req_obj) ? req_obj : XriResolver::ParseInputParameters.new
27
+ self.new(xri, req, stack)
28
+ end
29
+ end
30
+
31
+ attr_reader :xri, :xrd_array, :resolved, :status_code, :client, :req
32
+
33
+ def initialize(xri, req, stack)
34
+ if XriParser::AuthorityParser === xri
35
+ @xri = xri
36
+ else
37
+ begin
38
+ @xri = XriParser::AuthorityParser.parse(xri)
39
+ rescue Exception
40
+ raise
41
+ end
42
+ end
43
+
44
+ @req = req
45
+ @stack = stack || Array.new
46
+
47
+ if @req.saml
48
+ sts = REXML::Element.new('Status')
49
+ sts.add_attribute('code', code = '201')
50
+ sts.text = 'SAML_NOT_IMPLEMENTED'
51
+ xrd = REXML::Element.new('XRD')
52
+ xrd << sts
53
+ @xrd_array = Array.new
54
+ @xrd_array << xrd
55
+ return
56
+ end
57
+
58
+ raise UNKNOWN_ROOT, @xri.root unless Hash === XRIRoot.authorities[@xri.root]
59
+ @client = HTTPClient.new
60
+ @client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE unless XRIRoot.authorities[@xri.root][:verify_server_cert]
61
+ @accept_header = 'application/xrds+xml'
62
+ @accept_header << ';https=true' if @req.https
63
+
64
+ @xrd_array = Array.new
65
+ @resolved = "#{@xri.root}"
66
+ @status_code = String.new ## status code of the last resolved subsegment
67
+ subsegments_resolved = 0
68
+
69
+ @authority_uris = XRIRoot.authorities[@xri.root][:authority_uris].map {|u| URI.parse(u)}
70
+
71
+ errorcode = '220'
72
+ errortext = ResponseStatus[errorcode]
73
+
74
+ @xri.subsegments.each do |subsegment|
75
+ begin
76
+ @authority_uris = resolve_next(subsegment)
77
+ rescue Exception => e
78
+ errorcode = '220'
79
+ errortext = e.message
80
+ break
81
+ end
82
+ subsegments_resolved += 1
83
+ if @authority_uris.empty? or !@authority_uris.all?{|r| URI === r}
84
+ if subsegments_resolved < @xri.subsegments.length
85
+ if ele = (@xrd_array.last.elements['Ref'] or @xrd_array.last.elements['Redirect'])
86
+ @newreq = @req.clone
87
+ @newreq.sep = true
88
+ @newreq.nodefault_t = true
89
+ @newreq.service_type = XRDType
90
+ if ele.name == 'Ref'
91
+ authsep = AuthorityResolver.resolve(ele.text, @newreq, @stack)
92
+ else
93
+ authsep = URIAuthorityResolver.new(@xri, @newreq, doappend(ele).text, @stack)
94
+ end
95
+ if authsep.resolved_successfully? and not authsep.to_urilist.empty?
96
+ @xrd_array << authsep.to_xrds.root
97
+ @authority_uris = authsep.to_urilist.collect { |uri| URI.parse(uri) }
98
+ end
99
+ else
100
+ errorcode = @req.https ? '231' : '221'
101
+ errortext = ResponseStatus[errorcode]
102
+ break
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ if subsegments_resolved < @xri.subsegments.length
109
+ sts = REXML::Element.new('Status')
110
+ sts.add_attribute('code', errorcode)
111
+ sts.text = errortext
112
+ qry = REXML::Element.new('Query')
113
+ qry.text = CGI.unescape(@xri.subsegments[subsegments_resolved]).unpack('C*').pack('U*')
114
+ xrd = REXML::Element.new('XRD')
115
+ xrd << qry
116
+ xrd << sts
117
+ @xrd_array << xrd
118
+ end
119
+
120
+ serverstatus!
121
+ completeuris!
122
+ canonical!
123
+ references!
124
+ redirects!
125
+ services!
126
+ end
127
+
128
+ def resolved_successfully?
129
+ @resolved == CGI.unescape(@xri.fully_qualified) and @status_code === '100'
130
+ end
131
+
132
+ def to_s
133
+ build_xrds(@xrd_array)
134
+ end
135
+
136
+ def to_xrds
137
+ REXML::Document.new(self.to_s)
138
+ end
139
+
140
+ def last_xrd
141
+ if @xrd_array.last.root.name == 'XRDS'
142
+ REXML::XPath.match(@xrd_array.last.root, "//*[local-name()='XRD']", XRDNS).last
143
+ else
144
+ @xrd_array.last.root
145
+ end
146
+ end
147
+
148
+ def last_xrd_doc
149
+ REXML::Document.new(self.last_xrd.to_s.unpack('U*').pack('C*'))
150
+ end
151
+
152
+ def doappend(uri, inplace=nil)
153
+ uri_txt = uri.text
154
+ uri_app = uri.attribute('append').value rescue 'none'
155
+
156
+ unless uri_app == 'none'
157
+ uri_txt.slice!(/\/$/)
158
+ uri_txt.insert(-1, '/')
159
+ end
160
+
161
+ case uri_app
162
+ when 'local'
163
+ uri_txt << @xri.local
164
+ when 'authority'
165
+ uri_txt << @xri.authority
166
+ when 'path'
167
+ uri_txt << @xri.path
168
+ when 'query'
169
+ uri_txt << @xri.query
170
+ when 'qxri'
171
+ uri_txt << @xri.qxri
172
+ end
173
+
174
+ if inplace
175
+ uri.text = uri_txt
176
+ uri.delete_attribute('append')
177
+ uri
178
+ else
179
+ newuri = REXML::Element.new('URI')
180
+ newuri.text = uri_txt
181
+ newuri
182
+ end
183
+ end
184
+
185
+ def to_urilist
186
+ SEPSelector.select(self, self.last_xrd, @req).to_urilist
187
+ end
188
+
189
+ private
190
+ Retn = Struct.new(:header, :content)
191
+ def fetch_uri(uri)
192
+ if defined? Cache
193
+ key = uri.host + uri.path + @accept_header
194
+ if result = Cache.get(key)
195
+ return Retn.new({'FROM_CACHE' => 'true'}, result)
196
+ end
197
+ end
198
+
199
+ begin
200
+ @client.get_with_redirect(uri, nil, {'Accept'=> @accept_header})
201
+ rescue Exception
202
+ raise
203
+ end
204
+ end
205
+
206
+ def get_xrd_status_code(xrd)
207
+ REXML::XPath.match(xrd, "*[local-name()='Status']/@code", XRDNS).first.to_s rescue nil
208
+ end
209
+
210
+ def get_xrd_status_text(xrd)
211
+ REXML::XPath.match(xrd, "*[local-name()='Status']", XRDNS).first.to_s rescue nil
212
+ end
213
+
214
+ def get_xrd_query_string(xrd)
215
+ REXML::XPath.match(xrd, "*[local-name()='Query']", XRDNS).first.text rescue nil
216
+ end
217
+
218
+ def get_xrd_next_authority(xrd)
219
+ auth_uri = Array.new
220
+ REXML::XPath.match(xrd, "*[local-name()='Service']", XRDNS).each do |svc|
221
+ if REXML::XPath.match(svc, "*[local-name()='Type']", XRDNS).any? {|x| x.text == XRDType}
222
+ REXML::XPath.match(svc, "*[local-name()='MediaType']", XRDNS).any? {|x| mediatypeok?(x.text)}
223
+ REXML::XPath.match(svc, "*[local-name()='URI']", XRDNS).each do |r|
224
+ if urip = (URI.parse(r.text) rescue nil) and %w{http https}.include?(urip.scheme)
225
+ unless @req.https and urip.scheme != 'https'
226
+ auth_uri << [(r.attribute('priority').value rescue 0xFFFF).to_i, r.text]
227
+ end
228
+ end
229
+ end
230
+ break
231
+ end
232
+ end
233
+ auth_uri.sort{ |a, b| Common.prioritize a[0], b[0] }.map{ |y| y[1] } unless auth_uri.empty?
234
+ end
235
+
236
+ def resolve_next(subsegment)
237
+ begin
238
+ uri = @authority_uris.shift
239
+ uri.path << '/'
240
+ hxri = uri.merge(subsegment)
241
+ resolved = fetch_uri(hxri)
242
+ xml = resolved.content.unpack("C*").pack("U*") #convert to UTF8
243
+ rescue Exception => e
244
+ unless @authority_uris.empty?
245
+ retry
246
+ else
247
+ raise XRIResolutionFailure, "Unable to resolve #{subsegment} because: #{e.message}"
248
+ end
249
+ end
250
+
251
+ begin
252
+ doc = REXML::Document.new(xml)
253
+ rescue Exception => e
254
+ raise XRIResolutionFailure, "#{e.message} -- #{xml}"
255
+ end
256
+
257
+ new_authority_uris = Array.new
258
+ current_xrd_array = Array.new
259
+ if doc.root.name == "XRDS"
260
+ REXML::XPath.each(doc, "//*[local-name()='XRD']", XRDNS) do |xrd|
261
+ @xrd_array << xrd
262
+ current_xrd_array << xrd
263
+ @status_code = get_xrd_status_code(xrd)
264
+ @status_text = get_xrd_status_text(xrd)
265
+ if @status_code == '100'
266
+ @resolved << get_xrd_query_string(xrd).unpack("U*").pack("C*")
267
+ new_authority_uris = get_xrd_next_authority(xrd).map {|u| URI.parse(u)} rescue []
268
+ end
269
+ end
270
+ if defined?(Cache) and (resolved.header['FROM_CACHE'] != 'true')
271
+ add_to_cache(resolved.header, current_xrd_array, hxri, xml)
272
+ end
273
+ return new_authority_uris
274
+ else
275
+ raise INVALID_XRDS, doc.root.name
276
+ end
277
+ end
278
+
279
+ def add_to_cache(hdr, current_xrd_array, hxri, xml)
280
+ header_exp = get_header_exp(hdr)
281
+ xrd_exp = get_xrd_exp(current_xrd_array)
282
+ if header_exp and xrd_exp
283
+ exp = (header_exp < xrd_exp) ? header_exp : xrd_exp
284
+ elsif header_exp
285
+ exp = header_exp
286
+ elsif xrd_exp
287
+ exp = xrd_exp
288
+ else
289
+ exp = Time.now.to_i + 3600
290
+ end
291
+ key = hxri.host + hxri.path + @accept_header
292
+ Cache.add(key, xml, exp)
293
+ end
294
+
295
+ def get_header_exp(hdr)
296
+ unless hdr['Cache-Control'].empty?
297
+ return nil if hdr['Cache-Control'].first.downcase == 'no-cache'
298
+ end
299
+ unless hdr['Expires'].empty?
300
+ a = ParseDate.parsedate(hdr['Expires'].first) rescue []
301
+ if a[6] =~ /\A(GMT|UTC|Z)\z/i
302
+ return Time.gm(*a).to_i
303
+ end
304
+ end
305
+ nil
306
+ end
307
+
308
+ def get_xrd_exp(xrd_ary)
309
+ exp_array = Array.new
310
+ xrd_ary.each do |xrd|
311
+ d = REXML::XPath.match(xrd, "*[local-name()='Expires']", XRDNS).first.text rescue ''
312
+ unless d.empty?
313
+ a = ParseDate.parsedate(d) rescue []
314
+ if a[6] =~ /\A(GMT|UTC|Z)\z/i
315
+ exp_array << Time.gm(*a).to_i
316
+ end
317
+ end
318
+ end
319
+ return exp_array.min
320
+ end
321
+
322
+ def build_xrds(xrd_array)
323
+ xrds = %Q{<XRDS xmlns="xri://$xrds" ref="xri://#{CGI.unescape(@xri.normalized)}">\n}
324
+ xrd_array.each {|xrd| xrds << "#{xrd.to_s.unpack('U*').pack('C*')}\n" }
325
+ xrds << "</XRDS>"
326
+ end
327
+
328
+ def verified?(piduri, ciduri)
329
+ begin
330
+ parsed_pid = XriParser::AuthorityParser.parse(piduri)
331
+ pid_root = parsed_pid.root
332
+ pid_segs = parsed_pid.subsegments
333
+ parsed_cid = XriParser::AuthorityParser.parse(ciduri)
334
+ cid_root = parsed_cid.root
335
+ cid_segs = parsed_cid.subsegments
336
+ pid_root == cid_root and pid_segs == cid_segs[0, pid_segs.length]
337
+ rescue
338
+ false
339
+ end
340
+ end
341
+
342
+ def mediatypeok?(type)
343
+ mimetype, *subparams = type.split(';').map {|h| h.strip.downcase}
344
+ verdict = (mimetype == 'application/xrds+xml')
345
+ verdict &&= @req.https ? (subparams.include?('https=true') or subparam.include?('https=1')) : true
346
+ verdict &&= @req.saml ? (subparams.include?('saml=true') or subparam.include?('saml=1')) : true
347
+ end
348
+
349
+ def references!
350
+ @xrd_array.collect! { |ele| Array.new.push(ele) }
351
+
352
+ # don't follow Ref's already done as part of authority resolution
353
+ seen = @xrd_array.collect{ |x| x[0].name == 'XRDS' ? 'xrds' : nil }.compact
354
+ startflag = (seen.length == 0)
355
+ @xrd_array.each do |xrd|
356
+ if xrd[0].name == 'XRDS'
357
+ startflag = true
358
+ next
359
+ end
360
+ next unless startflag
361
+
362
+ REXML::XPath.match(xrd[0], "//*[local-name()='Ref']", XRDNS).each do |ref|
363
+ if @req.refs.nil? or @req.refs.downcase == 'true' or (@req.refs =~ /\d+/ and @req.refs.to_i > 0)
364
+ @newreq = @req.clone
365
+ @newreq.refs = (@req.refs.to_i > 0 and (lessone = @req.refs.to_i - 1)) ? lessone.to_s : @req.refs
366
+ newxri = XriParser::AuthorityParser.parse(ref.text)
367
+ newxri.path = @xri.path if @xri.path
368
+
369
+ @stack << @xri.authority
370
+ unless @stack.include?(newxri.authority)
371
+ refresolver = AuthorityResolver.resolve(newxri, @newreq, @stack)
372
+ if refresolver.resolved_successfully?
373
+ xrd << REXML::Document.new(refresolver.to_s).root
374
+ end
375
+ else
376
+ REXML::XPath.match(xrd[0], "*[local-name()='Status']", XRDNS).each do |status|
377
+ status.add_attribute('refs', 'false')
378
+ status.text = 'REF_CYCLE_DETECTED'
379
+ end
380
+ end
381
+ @stack.pop
382
+ else
383
+ REXML::XPath.match(xrd[0], "*[local-name()='Status']", XRDNS).each do |status|
384
+ status.add_attribute('refs', 'false')
385
+ end
386
+ end
387
+ end
388
+ end
389
+
390
+ @xrd_array.flatten!
391
+ end
392
+
393
+ def redirects!
394
+ @xrd_array.collect! { |ele| Array.new.push(ele) }
395
+
396
+ # don't follow Redirect's already done as part of authority resolution
397
+ seen = @xrd_array.collect{ |x| x[0].name == 'XRDS' ? 'xrds' : nil }.compact
398
+ startflag = (seen.length == 0)
399
+ @xrd_array.each do |xrd|
400
+ if xrd[0].name == 'XRDS'
401
+ startflag = true
402
+ next
403
+ end
404
+ next unless startflag
405
+
406
+ REXML::XPath.match(xrd[0], "//*[local-name()='Redirect']", XRDNS).each do |red|
407
+ unless @stack.include?(reduri = doappend(red).text)
408
+ @stack << reduri
409
+ redres = URIAuthorityResolver.new(@xri, @req, reduri, @stack)
410
+ @stack.pop
411
+ else
412
+ responsecode = '250'
413
+ xrd[0].elements['Status'].text = 'REDIRECT_CYCLE_DETECTED'
414
+ xrd[0].elements['Status'].add_attribute('code', responsecode)
415
+ next
416
+ end
417
+
418
+ # do synonym verification
419
+ synsok = true
420
+ synlst = %w{ProviderID LocalID EquivID CanonicalID CanonicalEquivID}
421
+ redres.last_xrd.elements.to_a.each do |ele|
422
+ if synlst.include?(ele.name)
423
+ unless xrd[0].elements[ele.name] and xrd[0].elements[ele.name].text == ele.text
424
+ synsok = false
425
+ break
426
+ end
427
+ end
428
+ end
429
+
430
+ # if verification succeeds, embed XRDS; else set return code
431
+ if synsok
432
+ xrd << redres.to_xrds
433
+ else
434
+ responsecode = '251'
435
+ xrd[0].elements['Status'].text = ResponseStatus[responsecode]
436
+ xrd[0].elements['Status'].add_attribute('code', responsecode)
437
+ end
438
+ end
439
+ end
440
+
441
+ @xrd_array.flatten!
442
+ end
443
+
444
+ def canonical!
445
+ return unless @req.cid
446
+ pid = nil; @xrd_array.each do |xrd|
447
+ next unless xrd.name == 'XRD'
448
+ if cid = xrd.elements['CanonicalID']
449
+ if uricid = (URI.parse(cid.text) rescue nil) and %w{http https}.include?(uricid.scheme)
450
+ # HTTP(S) URI Verification Rules (13.2.1)
451
+ uriqry = URI.parse(xrd.elements['Query'].text) rescue nil
452
+ uricid.fragment = nil
453
+ xrd.elements['Status'].add_attribute('cid', (uricid == uriqry) ? 'verified' : 'failed')
454
+ else
455
+ # XRI Verification Rules (13.2.2)
456
+ pid ||= xrd.elements['ProviderID'] || REXML::Element.new('ProviderID')
457
+ xrd.elements['Status'].add_attribute('cid', verified?(pid.text, cid.text) ? 'verified' : 'failed')
458
+ end
459
+ else
460
+ xrd.elements['Status'].add_attribute('cid', 'absent')
461
+ end
462
+ pid = xrd.elements['Status'].attribute('cid').value == 'verified' ? cid : nil
463
+ end
464
+
465
+ # perform CanonicalEquivID verification on last XRD in resolution chain
466
+ xrd = @xrd_array.last
467
+ if cid = xrd.elements['CanonicalID'] and ceid = xrd.elements['CanonicalEquivID']
468
+ if xrd.elements['Status'].attributes['cid'] == 'verified'
469
+ # default to CEID failed
470
+ xrd.elements['Status'].add_attribute('ceid', 'failed')
471
+
472
+ # resolve CanonicalEquivID and test for remote EquivID == local CanonicalEquivID
473
+ if ceiu = (URI.parse(ceid.text) rescue nil) and %w{http https}.include?(ceiu.scheme)
474
+ ceiudoc = REXML::Document.new(@client.get_content(ceiu)) rescue nil
475
+ lastxrd = ceiudoc.elements.to_a("//*[local-name()='XRD']").last if ceiudoc
476
+ else
477
+ ceidresolver = AuthorityResolver.resolve(ceid.text) rescue nil
478
+ lastxrd = ceidresolver.last_xrd if ceidresolver.resolved_successfully?
479
+ end
480
+
481
+ # test for remote EquivID == local CanonicalEquivID
482
+ if lastxrd and lastxrd.elements.to_a('EquivID').any? {|x| x.text == cid.text}
483
+ xrd.elements['Status'].add_attribute('ceid', 'verified')
484
+ end
485
+ end
486
+ end
487
+ end
488
+
489
+ def completeuris!
490
+ return unless @req.uric
491
+ @xrd_array.each do |xrd|
492
+ next unless xrd.name == 'XRD'
493
+ xpath = "//*[(local-name()='URI')|(local-name()='Redirect')]"
494
+ REXML::XPath.match(xrd, xpath, XRDNS).collect!{|uri| doappend(uri, "inplace") }
495
+ end
496
+ end
497
+
498
+ def serverstatus!
499
+ @xrd_array.each do |xrd|
500
+ next unless xrd.name == 'XRD'
501
+ serverstatus = REXML::Element.new('ServerStatus')
502
+ if xrd.elements['Status']
503
+ serverstatus.text = xrd.elements['Status'].text
504
+ serverstatus.add_attributes(xrd.elements['Status'].attributes)
505
+ end
506
+
507
+ newxrdeles = xrd.elements.partition do |x|
508
+ after = xrd.elements['Status'] || xrd.elements['Query']
509
+ xrd.elements.index(x) <= xrd.elements.index(after)
510
+ end
511
+
512
+ xrd.elements.delete_all('*')
513
+ newxrdeles[0].each { |x| xrd.elements << x }
514
+ xrd.elements << serverstatus
515
+ newxrdeles[1].each { |x| xrd.elements << x }
516
+ end
517
+ end
518
+
519
+ def noprefix(xri) xri[/(^xri:\/\/)*(.*$)/, 2]; end
520
+
521
+ def services!
522
+ return unless @req.sep
523
+
524
+ ## make list of XRD elements
525
+ xrd_list = @xrd_array.clone
526
+ xrd_list.delete_if { |xrd| xrd.name == 'XRDS' }
527
+
528
+ ## make list of XRDS elements
529
+ xrds_list = @xrd_array.clone
530
+ xrds_list.delete_if { |xrd| not xrd.name == 'XRDS' }
531
+
532
+ ## do SEP selection on last XRD
533
+ seplist = SEPSelector.select(self, xrd_list.last, @req)
534
+
535
+ if seplist.empty?
536
+ ## no Service elements selected, so delete all Service/Ref(s) or Service/Redirect(s)
537
+ xpath = "*[local-name()='Service']/*[(local-name()='Ref')|(local-name()='Redirect')]"
538
+ svclist = REXML::XPath.match(xrd_list.last, xpath, XRDNS).collect do |ele|
539
+ ele.name == 'Ref' ? noprefix(ele.text) : doappend(ele).text
540
+ end
541
+ unless svclist.empty?
542
+ xrds_list.delete_if do |xrds|
543
+ svclist.include?(noprefix(xrds.root.attributes['ref'] || xrds.root.attributes['redirect']))
544
+ end
545
+ end
546
+ else
547
+ ## Service elements selected
548
+ unless seplist.to_urilist.empty?
549
+ ## Service/URI selected at XRD level, so don't follow any Ref(s) or Redirect(s)
550
+ xrds_list.clear
551
+ else
552
+ ## Service/Ref or Service/Redirect selected at XRD level, so follow them
553
+ svclist = Array.new
554
+ seplist.to_reflist.collect { |svclistxri| noprefix(svclistxri) }.each { |svc| svclist << svc }
555
+ seplist.to_redlist.collect { |svclisturi| svclisturi }.each { |svc| svclist << svc }
556
+ unless svclist.empty?
557
+ xrds_list.delete_if do |xrds|
558
+ not svclist.include?(noprefix(xrds.root.attributes['ref'] || xrds.root.attributes['redirect']))
559
+ end
560
+ end
561
+ end
562
+ end
563
+
564
+ ## purge XRDS in which no Service/URI elements are present
565
+ xrds_list.delete_if do |xrds|
566
+ lastxrd = REXML::XPath.match(xrds, "//*[local-name()='XRD']", XRDNS).last
567
+ numuris = REXML::XPath.match(lastxrd, "//*[local-name()='Service']/*[local-name()='URI']", XRDNS).length
568
+ numuris == 0
569
+ end
570
+
571
+ ## break ties among selected Ref(s) or Redirect(s) by priority
572
+ chosenxrds = nil
573
+ unless xrds_list.length > 1
574
+ chosenxrds = xrds_list.first
575
+ else
576
+ reflist = Array.new
577
+ xpath = "//*[(local-name()='Ref')|(local-name()='Redirect')]"
578
+ REXML::XPath.match(xrd_list.last, xpath, XRDNS).each do |ref|
579
+ reflist << [(ref.attribute('priority') ? ref.attribute('priority').value : 0xFFFF), ref]
580
+ end
581
+ reflist.sort! { |x, y| Common.prioritize x[0], y[0] }
582
+ reflist.each do |ref|
583
+ xrds_list.each do |xrds|
584
+ xrdsid = xrds.root.attributes['ref'] || xrds.root.attributes['redirect']
585
+ refsid = ref[1].name == 'Ref' ? ref[1].text : doappend(ref[1]).text
586
+ if noprefix(xrdsid) == noprefix(refsid)
587
+ chosenxrds = xrds
588
+ break
589
+ end
590
+ end
591
+ break if chosenxrds
592
+ end
593
+ end
594
+
595
+ ## save SEP selection results to object state
596
+ @xrd_array = xrd_list[0..-2]
597
+ @xrd_array << seplist.to_xrd
598
+ @xrd_array << chosenxrds if chosenxrds
599
+ end
600
+ end
601
+
602
+ ResponseStatus = {
603
+ '100' => 'SUCCESS',
604
+ '200' => 'PERM_FAIL',
605
+ '201' => 'NOT_IMPLEMENTED',
606
+ '202' => 'LIMIT_EXCEEDED',
607
+ '210' => 'INVALID_INPUT',
608
+ '211' => 'INVALID_QXRI',
609
+ '212' => 'INVALID_RES_MEDIA_TYPE',
610
+ '213' => 'INVALID_SEP_TYPE',
611
+ '214' => 'INVALID_SEP_MEDIA_TYPE',
612
+ '215' => 'UNKNOWN_ROOT',
613
+ '220' => 'AUTH_RES_ERROR',
614
+ '221' => 'AUTH_RES_NOT_FOUND',
615
+ '222' => 'QUERY_NOT_FOUND',
616
+ '223' => 'UNEXPECTED_XRD',
617
+ '230' => 'TRUSTED_RES_ERROR',
618
+ '231' => 'HTTPS_RES_NOT_FOUND',
619
+ '232' => 'SAML_RES_NOT_FOUND',
620
+ '233' => 'HTTPS+SAML_RES_NOT_FOUND',
621
+ '234' => 'UNVERIFIED_SIGNATURE',
622
+ '240' => 'SEP_SELECTION_ERROR',
623
+ '241' => 'SEP_NOT_FOUND',
624
+ '250' => 'REDIRECT_FAILED',
625
+ '251' => 'REDIRECT_SYNONYM_VERIFICATION_FAILED',
626
+ '300' => 'TEMPORARY_FAIL',
627
+ '301' => 'TIMEOUT_ERROR',
628
+ '320' => 'NETWORK_ERROR',
629
+ '321' => 'UNEXPECTED_RESPONSE',
630
+ '322' => 'INVALID_XRDS'
631
+ }
632
+
633
+ class XRIResolutionFailure < RuntimeError; end
634
+ class INVALID_XRDS < XRIResolutionFailure; end
635
+ class UNKNOWN_ROOT < XRIResolutionFailure; end
636
+
637
+ class SEPSelector
638
+ ## usage:
639
+ ## SEPList = XriResolver::SEPSelector.select(xrds, xrd, inputs)
640
+ ##
641
+ ## where xrds is a valid XriResolver::AuthorityResolver object
642
+ ## where inputs is a valid XriResolver::ParseInputParameters object
643
+ ##
644
+ ## Returns an SEPList object corresponding to the selected SEPs, ordered by priority
645
+ ##
646
+ ## (literally implements pseudocode in XRI Resolution 2.0 WD11 ED03 Section 10.6)
647
+ ##
648
+ @@sel_categories = ['Path', 'Type', 'MediaType'].freeze
649
+ @@match_types = ['Positive', 'Default', 'Matched'].freeze
650
+
651
+ def SEPSelector.select(xrds, xrd, inputs)
652
+ ## initialize arrays
653
+ selected_set = SEPList.new(xrds, xrd)
654
+ default_set = SEPList.new(xrds, xrd)
655
+
656
+ ## initialize nodefault to false
657
+ nodefault = Hash.new
658
+ @@sel_categories.each do |sel_category|
659
+ nodefault[sel_category] = false
660
+ end
661
+
662
+ ## set nodefault according to subparameters
663
+ nodefault['Path'] = inputs.nodefault_p
664
+ nodefault['Type'] = inputs.nodefault_t
665
+ nodefault['MediaType'] = inputs.nodefault_m
666
+
667
+ ## normalize path
668
+ path = (xrds.xri.path[0] == ?/) ? xrds.xri.path[1..-1] : xrds.xri.path ## remove leading '/'
669
+ path = nil if path.empty? ## per XRI Resolution WD11 ED03 Section 6.1.1
670
+
671
+ ## set content values
672
+ content = Hash.new
673
+ content['Path'] = path
674
+ content['Type'] = inputs.service_type
675
+ content['MediaType'] = inputs.service_media_type
676
+
677
+ ## "FOR EACH SEP"
678
+ REXML::XPath.match(xrd, "*[local-name()='Service']", XRDNS).each do |sep|
679
+ ## "CREATE set of SEL match flags"
680
+ ## "SET all flags to FALSE"
681
+ flag = Hash.new
682
+ @@match_types.each do |match_type|
683
+ flag[match_type] = Hash.new
684
+ @@sel_categories.each do |sel_category|
685
+ flag[match_type][sel_category] = false
686
+ end
687
+ end
688
+
689
+ ## we now have
690
+ ## flag = {"Default"=>{"MediaType"=>false, "Type"=>false, "Path"=>false},
691
+ ## "Positive"=>{"MediaType"=>false, "Type"=>false, "Path"=>false},
692
+ ## "Matched"=>{"MediaType"=>false, "Type"=>false, "Path"=>false}}
693
+
694
+ ## "FOR EACH SEL of category x (where x=Type, Path or Mediatype)"
695
+ REXML::XPath.match(sep, "*[(local-name()='Type')|(local-name()='Path')|(local-name()='MediaType')]", XRDNS).each do |sel|
696
+ ## record match result on this sel
697
+ this_sel_positive_match = false
698
+ this_sel_default_match = false
699
+
700
+ ## "SET Matched.x=TRUE"
701
+ flag['Matched'][sel.name] = true
702
+
703
+ ## determine match
704
+ if sel.attributes['match'] and sel.attributes['match'] != 'content'
705
+ ## determine match per XRI Resolution WD11 ED03 Section 10.3.2
706
+ case sel.attributes['match']
707
+ when 'any'
708
+ this_sel_positive_match = true
709
+ when 'default'
710
+ this_sel_default_match = true
711
+ when 'non-null'
712
+ if content[sel.name]
713
+ this_sel_positive_match = true
714
+ end
715
+ when 'null'
716
+ unless content[sel.name]
717
+ this_sel_positive_match = true
718
+ end
719
+ end
720
+ else
721
+ ## if no match attribute, see if there's a content match per 10.3.6, 10.3.7 and 10.3.8
722
+ ## TODO: implement normalization and other rules per these three sections
723
+ if sel.text and content[sel.name]
724
+ if sel.text.downcase == content[sel.name].downcase
725
+ this_sel_positive_match = true
726
+ end
727
+ end
728
+ end
729
+
730
+ ## "IF match on this SEL is POSITIVE"
731
+ if this_sel_positive_match
732
+ if sel.attributes['select'] == 'true'
733
+ ## sec 10.4.2
734
+ selected_set << [sep]
735
+ break
736
+ else
737
+ flag['Positive'][sel.name] = true
738
+ next
739
+ end
740
+ elsif this_sel_default_match
741
+ ## if we have already found a positive SEL with the same name - sec. 10.3.5
742
+ if flag['Positive'][sel.name]
743
+ next
744
+ elsif nodefault[sel.name]
745
+ next
746
+ else
747
+ flag['Default'][sel.name] = true
748
+ next
749
+ end
750
+ else
751
+ next
752
+ end
753
+
754
+ ## "End FOR EACH SEL"
755
+ end
756
+
757
+ ## "IF Matched.x=FALSE" (Sec 10.3.3)
758
+ flag['Matched'].each do |selname, value|
759
+ unless value ## this SEL hasn't been matched, i.e. was missing
760
+ flag['Default'][selname] = true unless nodefault[selname]
761
+ end
762
+ end
763
+
764
+ ## "IF Positive.Type=TRUE AND [...]"
765
+ if flag['Positive']['Type'] and
766
+ flag['Positive']['Path'] and
767
+ flag['Positive']['MediaType']
768
+ selected_set << [sep]
769
+ next
770
+ end
771
+
772
+ if (flag['Positive']['Type'] or flag['Default']['Type']) and
773
+ (flag['Positive']['Path'] or flag['Default']['Path']) and
774
+ (flag['Positive']['MediaType'] or flag['Default']['MediaType'])
775
+ default_set << [sep, flag.dup]
776
+ end
777
+
778
+ ## "END FOR EACH SEP"
779
+ end
780
+
781
+ ## "IF SELECTED SET != EMPTY
782
+ unless selected_set.empty?
783
+ return selected_set
784
+ end
785
+
786
+ ## "IF DEFAULT SET != EMPTY ;see 10.5.2"
787
+ unless default_set.empty?
788
+ default_set.each do |sep, flag|
789
+ ## two positive flags
790
+ if (flag['Positive']['Type'] and flag['Positive']['Path']) or
791
+ (flag['Positive']['Type'] and flag['Positive']['MediaType']) or
792
+ (flag['Positive']['Path'] and flag['Positive']['MediaType'])
793
+ selected_set << [sep]
794
+ end
795
+ end
796
+
797
+ ## "IF SELECTED SET != EMPTY
798
+ unless selected_set.empty?
799
+ return selected_set
800
+ end
801
+
802
+ ## "FOR EACH SEP IN DEFAULT SET"
803
+ default_set.each do |sep, flag|
804
+ ## one positive flag
805
+ if flag['Positive']['Type'] or
806
+ flag['Positive']['Path'] or
807
+ flag['Positive']['MediaType']
808
+ selected_set << [sep]
809
+ end
810
+ end
811
+ end
812
+
813
+ ## "IF SELECTED SET != EMPTY
814
+ unless selected_set.empty?
815
+ ## there was one positive flag in the default set
816
+ return selected_set
817
+ else
818
+ ## all default flags
819
+ return default_set
820
+ end
821
+ ## "RETURN EMPTY SET"
822
+ return []
823
+ end
824
+ end
825
+
826
+ class SEPList < Array
827
+ def initialize(xrds, xrd)
828
+ @xrds = xrds
829
+ @xrd = xrd
830
+ end
831
+
832
+ def to_urilist
833
+ to_list('URI')
834
+ end
835
+
836
+ def to_reflist
837
+ to_list('Ref')
838
+ end
839
+
840
+ def to_redlist
841
+ to_list('Redirect')
842
+ end
843
+
844
+ def uris?
845
+ not to_list('URI').empty?
846
+ end
847
+
848
+ def refs?
849
+ not to_list('Ref').empty?
850
+ end
851
+
852
+ def reds?
853
+ not to_list('Redirect').empty?
854
+ end
855
+
856
+ def to_xrd
857
+ ## sort SEPs by value of "priority" attribute
858
+ seplist = Array.new
859
+ self.each do |sep, flag|
860
+ sep_pri = sep.attribute('priority') ? sep.attribute('priority').value : 0xFFFF
861
+ seplist << [sep_pri, sep]
862
+ end
863
+ seplist.sort! { |x, y| Common.prioritize x[0], y[0] }
864
+
865
+ ## remove all existing xrd:Service elements
866
+ newxrd = @xrd.clone
867
+ REXML::XPath.match(@xrd, "*[not(local-name()='Service')]", XRDNS).each do |ele|
868
+ newxrd << ele
869
+ end
870
+
871
+ ## re-add selected and sorted xrd:Service elements per 6.2.2
872
+ priority = 0
873
+ seplist.each do |sep_pri, sep|
874
+ sep.delete_attribute('priority')
875
+ sep.add_attribute('priority', priority)
876
+ priority += 1
877
+ newxrd << sep
878
+ end
879
+ REXML::Document.new(newxrd.to_s)
880
+ end
881
+
882
+ private
883
+ def to_list(elename)
884
+ ## sort SEPs, and URIs within them, by value of "priority" attribute
885
+ seplist = Array.new
886
+ self.each do |sep, flag|
887
+ sep_pri = sep.attribute('priority') ? sep.attribute('priority').value : 0xFFFF
888
+ sep_urilist = Array.new
889
+ REXML::XPath.match(sep, "*[local-name()='#{elename}']", XRDNS).each do |uri|
890
+ uri_pri = uri.attribute('priority') ? uri.attribute('priority').value : 0xFFFF
891
+ sep_urilist << [uri_pri, uri]
892
+ end
893
+ sep_urilist.sort! { |x, y| Common.prioritize x[0], y[0] }
894
+ seplist << [sep_pri, sep_urilist]
895
+ end
896
+ seplist.sort! { |x, y| Common.prioritize x[0], y[0] }
897
+
898
+ ## build and return URI list per XRI Resolution Specification Section 10.7
899
+ urilist = Array.new
900
+ seplist.each do |sep|
901
+ sep[1].each do |uri|
902
+ urilist << @xrds.doappend(uri[1]).text
903
+ end
904
+ end
905
+ urilist
906
+ end
907
+ end
908
+
909
+ class ParseInputParameters
910
+ attr_accessor :resolution_media_type,
911
+ :service_media_type,
912
+ :service_type,
913
+ :uric,
914
+ :sep,
915
+ :cid,
916
+ :nodefault_p,
917
+ :nodefault_t,
918
+ :nodefault_m,
919
+ :refs,
920
+ :https,
921
+ :saml
922
+ def initialize(params={}, accept_headers="")
923
+ if params.instance_of?(Hash)
924
+ symparams = Hash.new
925
+ params.each { |k,v| symparams[k.to_sym] = v }
926
+ params = symparams
927
+ end
928
+ param_inputs = parse_query_params(params)
929
+ header_inputs = parse_accept_headers(accept_headers)
930
+
931
+ unless is_blank?(param_inputs[:res_media_type])
932
+ @resolution_media_type = param_inputs[:res_media_type]
933
+ @uric = has_true_value?(param_inputs[:uric]) ? true : false
934
+ @sep = has_true_value?(param_inputs[:sep]) ? true : false
935
+ @cid = has_false_value?(param_inputs[:cid]) ? false : true
936
+ @nodefault_p = has_true_value?(param_inputs[:nodefault_p]) ? true : false
937
+ @nodefault_t = has_true_value?(param_inputs[:nodefault_t]) ? true : false
938
+ @nodefault_m = has_true_value?(param_inputs[:nodefault_m]) ? true : false
939
+ @refs = param_inputs[:refs]
940
+ @https = has_true_value?(param_inputs[:https]) ? true : false
941
+ @saml = has_true_value?(param_inputs[:saml]) ? true : false
942
+ else
943
+ @resolution_media_type = header_inputs[:res_media_type]
944
+ @uric = has_true_value?(header_inputs[:uric]) ? true : false
945
+ @sep = has_true_value?(header_inputs[:sep]) ? true : false
946
+ @cid = has_false_value?(header_inputs[:cid]) ? false : true
947
+ @nodefault_p = has_true_value?(header_inputs[:nodefault_p]) ? true : false
948
+ @nodefault_t = has_true_value?(header_inputs[:nodefault_t]) ? true : false
949
+ @nodefault_m = has_true_value?(header_inputs[:nodefault_m]) ? true : false
950
+ @refs = header_inputs[:refs]
951
+ @https = has_true_value?(header_inputs[:https]) ? true : false
952
+ @saml = has_true_value?(header_inputs[:saml]) ? true : false
953
+ end
954
+
955
+ @service_media_type = param_inputs[:service_media_type]
956
+ @service_type = param_inputs[:service_type]
957
+
958
+ ## force sep=true if RMT is text/uri-list or unspecified
959
+ @sep = true if @resolution_media_type == 'text/uri-list' or is_blank?(@resolution_media_type)
960
+ end
961
+
962
+ def parse_query_params(params)
963
+ inputs = {:res_media_type => ""}
964
+ ## get Resolution Media type
965
+ if params.has_key?(:_xrd_r)
966
+ parsedrof = params[:_xrd_r].split(';') rescue []
967
+ inputs[:res_media_type] = parsedrof.shift.tr(' ', '+') rescue ''
968
+ parsedrof.each do |part|
969
+ k,v = part.downcase.split('=')
970
+ inputs["#{k}".to_sym] = v unless is_blank?(k)
971
+ end
972
+ end
973
+ ## get Service Type selection element
974
+ inputs[:service_type] = (params.has_key?(:_xrd_t)) ? params[:_xrd_t].tr(' ', '+') : nil
975
+ ## get Service Media Type selection element
976
+ inputs[:service_media_type] = (params.has_key?(:_xrd_m)) ? params[:_xrd_m].tr(' ', '+') : nil
977
+
978
+ inputs
979
+ end
980
+
981
+ def parse_accept_headers(accept_headers)
982
+ valid_headers = ['application/xrds+xml', 'application/xrd+xml', 'text/uri-list']
983
+ res_inputs = {:res_media_type => ""}
984
+ headers = accept_headers.split(',').map {|h| h.strip}
985
+ headers.each do |hdr|
986
+ hdr_parts = hdr.split(';')
987
+ if valid_headers.include?(hdr_parts.first)
988
+ res_inputs[:res_media_type] = hdr_parts.shift
989
+ hdr_parts.each do |part|
990
+ k,v = part.downcase.split('=')
991
+ res_inputs["#{k}".to_sym] = v unless is_blank?(k)
992
+ end
993
+ break
994
+ end
995
+ end
996
+ res_inputs
997
+ end
998
+
999
+ def has_true_value?(test)
1000
+ test == 'true' or test == '1'
1001
+ end
1002
+
1003
+ def has_false_value?(test)
1004
+ test == 'false' or test == '0'
1005
+ end
1006
+
1007
+ def is_blank?(test)
1008
+ test.empty? or test =~ /\A\s*\z/
1009
+ end
1010
+ end
1011
+
1012
+ class URIAuthorityResolver < AuthorityResolver
1013
+ def initialize(xri, req, uri, stack)
1014
+ if uri = (URI.parse(uri) rescue nil) and %w{http https}.include?(uri.scheme)
1015
+ @xri = xri
1016
+ @req = req
1017
+ @uri = uri
1018
+ @stack = stack || Array.new
1019
+ @client = HTTPClient.new
1020
+ uriget = fetch_uri(uri)
1021
+ urixml = uriget.content.unpack("C*").pack("U*")
1022
+ uridoc = REXML::Document.new(urixml).root
1023
+ @xrd_array = REXML::XPath.match(uridoc, "*", XRDNS)
1024
+
1025
+ serverstatus!
1026
+ completeuris!
1027
+ canonical!
1028
+ references!
1029
+ redirects!
1030
+ services!
1031
+ end
1032
+ end
1033
+
1034
+ def to_s
1035
+ self.to_xrds.to_s
1036
+ end
1037
+
1038
+ def to_xrds
1039
+ xrds = REXML::Element.new('XRDS')
1040
+ xrds.add_namespace('xri://$xrds')
1041
+ xrds.add_attribute('redirect', @uri)
1042
+ @xrd_array.each { |ele| xrds << ele }
1043
+ xrds
1044
+ end
1045
+
1046
+ def resolved_successfully?
1047
+ true
1048
+ end
1049
+ end
1050
+
1051
+ class Common
1052
+ def Common.prioritize(x, y)
1053
+ (x.to_i == y.to_i) ? (rand(3) - 1) : (x.to_i <=> y.to_i)
1054
+ end
1055
+ end
1056
+ end