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