discodactyl 0.3.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.
- data/AUTHORS +1 -0
- data/CHANGELOG +452 -0
- data/COPYING +662 -0
- data/Gemfile +8 -0
- data/INSTALL +26 -0
- data/MANIFEST +30 -0
- data/NEWS +31 -0
- data/README +63 -0
- data/Rakefile +23 -0
- data/TODO +24 -0
- data/bin/webfinger +62 -0
- data/discodactyl.gemspec +23 -0
- data/lib/discodactyl.rb +9 -0
- data/lib/discodactyl/acct_uri.rb +82 -0
- data/lib/discodactyl/host_meta.rb +34 -0
- data/lib/discodactyl/link_header.rb +45 -0
- data/lib/discodactyl/resource_discovery.rb +102 -0
- data/lib/discodactyl/uri_template.rb +32 -0
- data/lib/discodactyl/xrd.rb +2 -0
- data/lib/discodactyl/xrd/document.rb +75 -0
- data/lib/discodactyl/xrd/link.rb +47 -0
- data/test/test_acct_uri.rb +57 -0
- data/test/test_helper.rb +17 -0
- data/test/test_host_meta.rb +37 -0
- data/test/test_link_header.rb +32 -0
- data/test/test_resource_discovery.rb +65 -0
- data/test/test_uri_template.rb +32 -0
- data/test/test_xrd_append.rb +93 -0
- data/test/test_xrd_link_parse.rb +97 -0
- data/test/test_xrd_parse.rb +146 -0
- metadata +164 -0
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'nokogiri'
|
3
|
+
require 'discodactyl/link_header'
|
4
|
+
|
5
|
+
module Discodactyl # :nodoc:
|
6
|
+
class ResourceDiscovery
|
7
|
+
class << self
|
8
|
+
|
9
|
+
# perform LRDD on the URI, returning all linked URIs which match
|
10
|
+
# the provided rel. Any URITemplates will be expanded with the
|
11
|
+
# params if they are provided
|
12
|
+
#--
|
13
|
+
# TODO: xri support: no host
|
14
|
+
# TODO: check if XRD/Property[@type=http://lrdd.net/priority/resource] to indicate resource-priority
|
15
|
+
# TODO: handle 3** status redirects
|
16
|
+
# TODO: maintain a security bit
|
17
|
+
# TODO: URIs for all discovery modes should be appended, not just returned
|
18
|
+
# TODO: rewrite this so it's just get links, which yields objects providing at least the xrd link interface (rel, href, type)
|
19
|
+
#++
|
20
|
+
def get_uris_by_rel(uri, rel, params = {})
|
21
|
+
begin
|
22
|
+
uri = URI.parse(uri.to_s) unless uri.respond_to?('open')
|
23
|
+
resource = uri.open
|
24
|
+
rescue OpenURI::HTTPError => e
|
25
|
+
status = e.io.status[0] # => 3xx, 4xx, or 5xx
|
26
|
+
|
27
|
+
# code = Net::HTTPResponse::CODE_TO_OBJ[status]
|
28
|
+
# if code == "303"
|
29
|
+
# 303 HTTPSeeOther
|
30
|
+
# if res.key? 'Link'
|
31
|
+
# links = res['Link']
|
32
|
+
# end
|
33
|
+
# elsif code == "401" # HTTPUnauthorized
|
34
|
+
# authenticate
|
35
|
+
# return get_uris_by_rel(uri)
|
36
|
+
# elsif code == "301" || status == "302"
|
37
|
+
# 300 HTTPMultipleChoice
|
38
|
+
# 301 HTTPMovedPermanently
|
39
|
+
# 302 HTTPFound
|
40
|
+
# 304 HTTPNotModified
|
41
|
+
# 305 HTTPUseProxy
|
42
|
+
# 307 HTTPTemporaryRedirect
|
43
|
+
# resource = URI.parse(resource['location']).open
|
44
|
+
# else
|
45
|
+
rescue NoMethodError
|
46
|
+
end
|
47
|
+
if resource
|
48
|
+
# check for link headers first
|
49
|
+
if resource.meta.key? 'Link'
|
50
|
+
uris = get_uris_by_rel_from_link_header(resource, rel)
|
51
|
+
end
|
52
|
+
|
53
|
+
unless uris
|
54
|
+
# then check for links in the document
|
55
|
+
if content_sniff(resource) == 'text/html'
|
56
|
+
uris = get_uris_by_rel_from_html(resource, rel)
|
57
|
+
# Atom
|
58
|
+
elsif content_sniff(resource) == 'application/atom+xml'
|
59
|
+
uris = get_uris_by_rel_from_atom(resource, rel)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
unless uris
|
64
|
+
host_meta = Discodactyl::HostMeta.from_uri uri
|
65
|
+
uris = host_meta.uris_by_rel(rel, params)
|
66
|
+
end
|
67
|
+
uris
|
68
|
+
end
|
69
|
+
|
70
|
+
def lrdd_discovery(uri)
|
71
|
+
get_uris_by_rel(uri, 'describedby')
|
72
|
+
end
|
73
|
+
|
74
|
+
def content_sniff(resource)
|
75
|
+
if resource.content_type == 'text/html'
|
76
|
+
type = 'text/html'
|
77
|
+
elsif resource.content_type == 'application/atom+xml'
|
78
|
+
type = 'application/atom+xml'
|
79
|
+
end
|
80
|
+
type
|
81
|
+
end
|
82
|
+
|
83
|
+
# take an HTTP response with a content-type of HTML,
|
84
|
+
# find all links by rel, and return each href
|
85
|
+
def get_uris_by_rel_from_html(resource, rel)
|
86
|
+
doc = Nokogiri::HTML(resource)
|
87
|
+
links = doc.xpath("//*[contains(@rel, \"#{rel}\")]")
|
88
|
+
uris = links.map {|link| link['href'] }
|
89
|
+
end
|
90
|
+
|
91
|
+
# take an HTTP response, find a link header
|
92
|
+
# with rel, and return its href
|
93
|
+
def get_uris_by_rel_from_link_header(response, rel)
|
94
|
+
links = [response.meta['Link']].flatten.collect {|link|
|
95
|
+
LinkHeader.parse(link);
|
96
|
+
}
|
97
|
+
link = links.find {|l| l[:rel].include? rel }
|
98
|
+
xrd = link[:href]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Discodactyl # :nodoc:
|
2
|
+
# Basic URI templates as used in XRD. Not to be confused with
|
3
|
+
# http://tools.ietf.org/html/draft-gregorio-uritemplate
|
4
|
+
class URITemplate
|
5
|
+
attr_accessor :pattern
|
6
|
+
def initialize(pattern)
|
7
|
+
@pattern = pattern
|
8
|
+
end
|
9
|
+
def to_uri(params)
|
10
|
+
require 'cgi'
|
11
|
+
uri = @pattern
|
12
|
+
while /\{%([^}]*)\}/ =~ uri
|
13
|
+
uri.gsub!($~[0], params[$~[1]])
|
14
|
+
end
|
15
|
+
while /\{([^}]*)\}/ =~ uri
|
16
|
+
uri.gsub!($~[0], CGI::escape(params[$~[1]].to_s))
|
17
|
+
end
|
18
|
+
uri
|
19
|
+
end
|
20
|
+
|
21
|
+
def ==(other)
|
22
|
+
return false unless other.class == self.class
|
23
|
+
return false unless other.instance_variables == self.instance_variables
|
24
|
+
self.instance_variables.each do |var|
|
25
|
+
self_var = self.instance_variable_get(var)
|
26
|
+
other_var = other.instance_variable_get(var)
|
27
|
+
return false unless self_var.eql?(other_var)
|
28
|
+
end
|
29
|
+
true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'discodactyl/xrd/link'
|
3
|
+
|
4
|
+
module Discodactyl # :nodoc:
|
5
|
+
module XRD # :nodoc:
|
6
|
+
XMLNS = {'xrd' => 'http://docs.oasis-open.org/ns/xri/xrd-1.0'}
|
7
|
+
class Document
|
8
|
+
class << self
|
9
|
+
def parse(string)
|
10
|
+
raw = Nokogiri::XML(string)
|
11
|
+
doc = self.new
|
12
|
+
|
13
|
+
doc.raw = raw
|
14
|
+
|
15
|
+
doc
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_accessor :raw
|
20
|
+
|
21
|
+
def escapeXPath(str)
|
22
|
+
inner = str.split('\'').join('\',"\'",\'')
|
23
|
+
outer = 'concat(\'\',\'%s\')' % inner
|
24
|
+
end
|
25
|
+
|
26
|
+
def linkelems_by_rel(rel)
|
27
|
+
path = '/xrd:XRD/xrd:Link[@rel=%s]'% escapeXPath(rel)
|
28
|
+
@raw.xpath path, XMLNS
|
29
|
+
end
|
30
|
+
|
31
|
+
def links_by_rel(rel)
|
32
|
+
linkelems_by_rel(rel).map {|e| Link.parse(e) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def uris_by_rel(rel, params = {})
|
36
|
+
links_by_rel(rel).map {|l| l.to_uri(params) }
|
37
|
+
end
|
38
|
+
|
39
|
+
# take an XML fragment for a link and append it to the document
|
40
|
+
def append(link)
|
41
|
+
initial_ids = ids
|
42
|
+
raw.root.add_child(link)
|
43
|
+
elem = Link.parse(raw.root.last_element_child)
|
44
|
+
elem.id = generate_tag_uri if elem.id.nil? || initial_ids.include?(elem.id)
|
45
|
+
elem
|
46
|
+
end
|
47
|
+
|
48
|
+
def links
|
49
|
+
raw.xpath('/xrd:XRD/xrd:Link', XMLNS).collect {|elem|
|
50
|
+
Link.parse(elem)
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def find_link_by_id(link_id)
|
55
|
+
links.find {|link| link.id == link_id}
|
56
|
+
end
|
57
|
+
|
58
|
+
def ids
|
59
|
+
links.map(&:id).reject(&:nil?)
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_s
|
63
|
+
raw.to_s
|
64
|
+
end
|
65
|
+
|
66
|
+
def generate_tag_uri
|
67
|
+
scheme = 'tag'
|
68
|
+
authority = 'dactylo.us'
|
69
|
+
date = Date.today.to_s
|
70
|
+
specific = "/xrd/link/#{rand(2**10)}"
|
71
|
+
"#{scheme}:#{authority},#{date}:#{specific}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'active_support/core_ext/object/misc'
|
3
|
+
require 'discodactyl/uri_template'
|
4
|
+
|
5
|
+
module Discodactyl # :nodoc:
|
6
|
+
module XRD # :nodoc:
|
7
|
+
class Link
|
8
|
+
class << self
|
9
|
+
def parse(element)
|
10
|
+
returning(link = self.new) do
|
11
|
+
link.rel = element['rel']
|
12
|
+
link.type = element['type']
|
13
|
+
link.href = element['href']
|
14
|
+
link.template = URITemplate.new(element['template']) unless link.href
|
15
|
+
link.raw = element
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_accessor :href, :template, :rel, :type, :raw
|
21
|
+
|
22
|
+
def to_uri(params = {})
|
23
|
+
@href || @template.to_uri(params)
|
24
|
+
end
|
25
|
+
|
26
|
+
def id
|
27
|
+
begin
|
28
|
+
@raw.attribute_with_ns('id', 'http://www.w3.org/XML/1998/namespace').value
|
29
|
+
rescue
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def id=(value)
|
34
|
+
@raw['xml:id'] = value
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_s
|
38
|
+
@raw.to_s
|
39
|
+
end
|
40
|
+
|
41
|
+
def ==(other)
|
42
|
+
(other.respond_to?('raw') && (@raw == other.raw)) ||
|
43
|
+
super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
libdir = File.expand_path('../../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
|
4
|
+
|
5
|
+
require "test/unit"
|
6
|
+
require "discodactyl/acct_uri"
|
7
|
+
|
8
|
+
class TestURIAcct < Test::Unit::TestCase
|
9
|
+
def test_parse_without_scheme
|
10
|
+
a = URI::ACCT.parse('user@host.example')
|
11
|
+
assert_equal('user', a.local_part)
|
12
|
+
assert_equal('host.example', a.host)
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_parse_with_scheme
|
16
|
+
a = URI::ACCT.parse('acct:user@host.example')
|
17
|
+
assert_equal('user', a.local_part)
|
18
|
+
assert_equal('host.example', a.host)
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_parse_uri_by_scheme
|
22
|
+
a = URI.parse('acct:user@host.example')
|
23
|
+
assert_equal('user', a.local_part)
|
24
|
+
assert_equal('host.example', a.host)
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_build_from_hash
|
28
|
+
a = URI::ACCT.build(:local_part => 'user',
|
29
|
+
:host => 'host.example')
|
30
|
+
assert_equal('user', a.local_part)
|
31
|
+
assert_equal('host.example', a.host)
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_build_from_opaque
|
35
|
+
a = URI::ACCT.build(:opaque => 'user@host.example')
|
36
|
+
assert_equal('user', a.local_part)
|
37
|
+
assert_equal('host.example', a.host)
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_build_from_array
|
41
|
+
a = URI::ACCT.build(['user','host.example'])
|
42
|
+
assert_equal('user', a.local_part)
|
43
|
+
assert_equal('host.example', a.host)
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
def test_to_s
|
48
|
+
a = URI::ACCT.build(:local_part => 'user',
|
49
|
+
:host => 'host.example')
|
50
|
+
assert_equal('acct:user@host.example', a.to_s)
|
51
|
+
end
|
52
|
+
def test_id
|
53
|
+
a = URI::ACCT.build(:local_part => 'user',
|
54
|
+
:host => 'host.example')
|
55
|
+
assert_equal('user@host.example', a.id)
|
56
|
+
end
|
57
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "test/unit"
|
2
|
+
module Test::Unit::Assertions
|
3
|
+
def assert_length(expected, enum, message = nil)
|
4
|
+
message = build_message message, '<?> is not length <?>', enum, expected
|
5
|
+
assert_equal expected, enum.length, message
|
6
|
+
end
|
7
|
+
|
8
|
+
def assert_include?(atom, enum, message = nil)
|
9
|
+
message = build_message message, '<?> does not include <?>.', enum, atom
|
10
|
+
assert enum.include?(atom), message
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Test::Unit::TestCase
|
15
|
+
require 'rr'
|
16
|
+
include RR::Adapters::TestUnit
|
17
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
libdir = File.expand_path('../../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
|
4
|
+
|
5
|
+
require "test/unit"
|
6
|
+
require "discodactyl/host_meta"
|
7
|
+
require "discodactyl/acct_uri"
|
8
|
+
|
9
|
+
class TestHostMeta < Test::Unit::TestCase
|
10
|
+
def test_get_uri_from_host
|
11
|
+
uri = 'host.example'
|
12
|
+
expected = URI.parse 'http://host.example/.well-known/host-meta'
|
13
|
+
assert_equal expected, Discodactyl::HostMeta.get_uri_from_uri(uri)
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_get_uri_from_http
|
17
|
+
uri = URI.parse 'http://host.example/some/path'
|
18
|
+
expected = URI.parse 'http://host.example/.well-known/host-meta'
|
19
|
+
assert_equal expected, Discodactyl::HostMeta.get_uri_from_uri(uri)
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_get_uri_from_acct
|
23
|
+
uri = URI.parse 'acct:user@host.example'
|
24
|
+
expected = URI.parse 'http://host.example/.well-known/host-meta'
|
25
|
+
assert_equal expected, Discodactyl::HostMeta.get_uri_from_uri(uri)
|
26
|
+
end
|
27
|
+
|
28
|
+
# def test_raise_meaningful_exception
|
29
|
+
# stub(io).status { [ '404', 'Not Found'] }
|
30
|
+
# stub(uri).open { raise OpenURI::HTTPError.new('foo', io)}
|
31
|
+
# stub(URI).parse('http://example.com/.well-known/host-meta') { uri }
|
32
|
+
#
|
33
|
+
# assert_raise Discodactyl::HostMetaHTTPError, "404 Not Found" do
|
34
|
+
# Discodactyl::HostMeta.from_uri(uri)
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
libdir = File.expand_path('../../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
|
4
|
+
|
5
|
+
require "test/unit"
|
6
|
+
require "discodactyl/link_header"
|
7
|
+
|
8
|
+
class TestLinkHeader < Test::Unit::TestCase
|
9
|
+
def test_parse_rel_and_title
|
10
|
+
link = '<http://example.com/TheBook/chapter2>; rel="previous"; title="previous chapter"'
|
11
|
+
expected = {:href => 'http://example.com/TheBook/chapter2', :rel => ['previous'], :title => 'previous chapter'}
|
12
|
+
assert_equal expected, Discodactyl::LinkHeader.parse(link)
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_parse_seperated_rels
|
16
|
+
link = '<http://example.org/>; rel=index; rel="start http://example.net/relation/other"'
|
17
|
+
expected = {:href => 'http://example.org/', :rel => ['index', 'start', 'http://example.net/relation/other']}
|
18
|
+
assert_equal expected, Discodactyl::LinkHeader.parse(link)
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_parse_simple_link
|
22
|
+
link = '</bar>; rel="http://example.com/profile1/foo"'
|
23
|
+
expected = {:href => '/bar', :rel => ['http://example.com/profile1/foo']}
|
24
|
+
assert_equal expected, Discodactyl::LinkHeader.parse(link)
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_parse_xrd_link
|
28
|
+
link = '<http://josephholsten.com/descriptor.xrd>; rel="describedby"; type="application/xrd+xml"'
|
29
|
+
expected = {:href => 'http://josephholsten.com/descriptor.xrd', :rel => ['describedby'], :type => 'application/xrd+xml' }
|
30
|
+
assert_equal expected, Discodactyl::LinkHeader.parse(link)
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
libdir = File.expand_path('../../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
|
4
|
+
testdir = File.expand_path('../../test', __FILE__)
|
5
|
+
$LOAD_PATH.unshift(testdir) unless $LOAD_PATH.include?(testdir)
|
6
|
+
|
7
|
+
require 'test_helper'
|
8
|
+
require "test/unit"
|
9
|
+
require "discodactyl/resource_discovery"
|
10
|
+
|
11
|
+
class TestResourceDiscovery < Test::Unit::TestCase
|
12
|
+
def test_get_uris_by_rel_from_html
|
13
|
+
raw = '<html><head><title></title><link rel="describedby" href="http://host.example/description.xrd"></head><body></body></html>'
|
14
|
+
|
15
|
+
uris = Discodactyl::ResourceDiscovery.get_uris_by_rel_from_html(raw, 'describedby')
|
16
|
+
|
17
|
+
assert_include? 'http://host.example/description.xrd', uris
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_get_uris_by_rel_from_html_with_multiple_rels
|
21
|
+
raw = '<html><head><title></title><link rel="also describedby" href="http://host.example/description.xrd"></head><body></body></html>'
|
22
|
+
|
23
|
+
uris = Discodactyl::ResourceDiscovery.get_uris_by_rel_from_html(raw, 'describedby')
|
24
|
+
|
25
|
+
assert_include? 'http://host.example/description.xrd', uris
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_get_uris_by_rel_from_html_via_disco
|
29
|
+
require 'ostruct'
|
30
|
+
raw = '<html><head><title></title><link rel="describedby" href="http://host.example/description.xrd"></head><body></body></html>'
|
31
|
+
response = FakeResp.new(raw)
|
32
|
+
response.meta = {}
|
33
|
+
response.content_type = 'text/html'
|
34
|
+
uri = OpenStruct.new(:open => response)
|
35
|
+
|
36
|
+
uris = Discodactyl::ResourceDiscovery.get_uris_by_rel(uri, 'describedby')
|
37
|
+
|
38
|
+
assert_include? 'http://host.example/description.xrd', uris
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_get_uris_by_rel_from_link_header
|
42
|
+
require 'ostruct'
|
43
|
+
header = '<http://host.example/description.xrd>; rel="describedby"'
|
44
|
+
response = OpenStruct.new(:meta => {'Link' => header})
|
45
|
+
|
46
|
+
uris = Discodactyl::ResourceDiscovery.get_uris_by_rel_from_link_header(response, 'describedby')
|
47
|
+
|
48
|
+
assert_equal 'http://host.example/description.xrd', uris
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_get_uris_by_rel_from_header
|
52
|
+
require 'ostruct'
|
53
|
+
header = '<http://host.example/description.xrd>; rel="describedby"'
|
54
|
+
response = OpenStruct.new(:meta => {'Link' => header})
|
55
|
+
uri = OpenStruct.new(:open => response)
|
56
|
+
|
57
|
+
uris = Discodactyl::ResourceDiscovery.get_uris_by_rel(uri, 'describedby')
|
58
|
+
|
59
|
+
assert_equal 'http://host.example/description.xrd', uris
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class FakeResp < String
|
64
|
+
attr_accessor :meta, :content_type
|
65
|
+
end
|