diaspora_federation 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +18 -0
- data/README.md +18 -0
- data/Rakefile +26 -0
- data/app/controllers/diaspora_federation/application_controller.rb +6 -0
- data/app/controllers/diaspora_federation/h_card_controller.rb +20 -0
- data/app/controllers/diaspora_federation/receive_controller.rb +35 -0
- data/app/controllers/diaspora_federation/webfinger_controller.rb +60 -0
- data/config/routes.rb +15 -0
- data/lib/diaspora_federation.rb +120 -0
- data/lib/diaspora_federation/engine.rb +15 -0
- data/lib/diaspora_federation/logging.rb +25 -0
- data/lib/diaspora_federation/version.rb +5 -0
- data/lib/diaspora_federation/web_finger.rb +14 -0
- data/lib/diaspora_federation/web_finger/exceptions.rb +19 -0
- data/lib/diaspora_federation/web_finger/h_card.rb +319 -0
- data/lib/diaspora_federation/web_finger/host_meta.rb +100 -0
- data/lib/diaspora_federation/web_finger/web_finger.rb +263 -0
- data/lib/diaspora_federation/web_finger/xrd_document.rb +181 -0
- data/lib/tasks/diaspora_federation_tasks.rake +4 -0
- data/lib/tasks/tests.rake +18 -0
- metadata +94 -0
@@ -0,0 +1,100 @@
|
|
1
|
+
|
2
|
+
module DiasporaFederation
|
3
|
+
module WebFinger
|
4
|
+
##
|
5
|
+
# Generates and parses Host Meta documents.
|
6
|
+
#
|
7
|
+
# This is a minimal implementation of the standard, only to the degree of what
|
8
|
+
# is used for the purposes of the Diaspora* protocol. (e.g. WebFinger)
|
9
|
+
#
|
10
|
+
# @example Creating a Host Meta document
|
11
|
+
# doc = HostMeta.from_base_url("https://pod.example.tld/")
|
12
|
+
# doc.to_xml
|
13
|
+
#
|
14
|
+
# @example Parsing a Host Meta document
|
15
|
+
# doc = HostMeta.from_xml(xml_string)
|
16
|
+
# webfinger_tpl = doc.webfinger_template_url
|
17
|
+
#
|
18
|
+
# @see http://tools.ietf.org/html/rfc6415 RFC 6415: "Web Host Metadata"
|
19
|
+
# @see XrdDocument
|
20
|
+
class HostMeta
|
21
|
+
private_class_method :new
|
22
|
+
|
23
|
+
# URL fragment to append to the base URL
|
24
|
+
WEBFINGER_SUFFIX = "webfinger?q={uri}"
|
25
|
+
|
26
|
+
##
|
27
|
+
# Returns the WebFinger URL that was used to build this instance (either from
|
28
|
+
# xml or by giving a base URL).
|
29
|
+
# @return [String] WebFinger template URL
|
30
|
+
def webfinger_template_url
|
31
|
+
@webfinger_url
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# Produces the XML string for the Host Meta instance with a +Link+ element
|
36
|
+
# containing the +webfinger_url+.
|
37
|
+
# @return [String] XML string
|
38
|
+
def to_xml
|
39
|
+
doc = XrdDocument.new
|
40
|
+
doc.links << {rel: "lrdd",
|
41
|
+
type: "application/xrd+xml",
|
42
|
+
template: @webfinger_url}
|
43
|
+
doc.to_xml
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# Builds a new HostMeta instance and constructs the WebFinger URL from the
|
48
|
+
# given base URL by appending HostMeta::WEBFINGER_SUFFIX.
|
49
|
+
# @return [HostMeta]
|
50
|
+
# @raise [InvalidData] if the webfinger url is malformed
|
51
|
+
def self.from_base_url(base_url)
|
52
|
+
raise ArgumentError, "base_url is not a String" unless base_url.instance_of?(String)
|
53
|
+
|
54
|
+
base_url += "/" unless base_url.end_with?("/")
|
55
|
+
webfinger_url = base_url + WEBFINGER_SUFFIX
|
56
|
+
raise InvalidData, "invalid webfinger url: #{webfinger_url}" unless webfinger_url_valid?(webfinger_url)
|
57
|
+
|
58
|
+
hm = allocate
|
59
|
+
hm.instance_variable_set(:@webfinger_url, webfinger_url)
|
60
|
+
hm
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Reads the given Host Meta XML document string and populates the
|
65
|
+
# +webfinger_url+.
|
66
|
+
# @param [String] hostmeta_xml Host Meta XML string
|
67
|
+
# @raise [InvalidData] if the xml or the webfinger url is malformed
|
68
|
+
def self.from_xml(hostmeta_xml)
|
69
|
+
data = XrdDocument.xml_data(hostmeta_xml)
|
70
|
+
raise InvalidData, "received an invalid xml" unless data.key?(:links)
|
71
|
+
|
72
|
+
webfinger_url = webfinger_url_from_xrd(data)
|
73
|
+
raise InvalidData, "invalid webfinger url: #{webfinger_url}" unless webfinger_url_valid?(webfinger_url)
|
74
|
+
|
75
|
+
hm = allocate
|
76
|
+
hm.instance_variable_set(:@webfinger_url, webfinger_url)
|
77
|
+
hm
|
78
|
+
end
|
79
|
+
|
80
|
+
##
|
81
|
+
# Applies some basic sanity-checking to the given URL
|
82
|
+
# @param [String] url validation subject
|
83
|
+
# @return [Boolean] validation result
|
84
|
+
def self.webfinger_url_valid?(url)
|
85
|
+
!url.nil? && url.instance_of?(String) && url =~ %r{^https?:\/\/.*\{uri\}}i
|
86
|
+
end
|
87
|
+
private_class_method :webfinger_url_valid?
|
88
|
+
|
89
|
+
##
|
90
|
+
# Gets the webfinger url from an XRD data structure
|
91
|
+
# @param [Hash] data extracted data
|
92
|
+
# @return [String] webfinger url
|
93
|
+
def self.webfinger_url_from_xrd(data)
|
94
|
+
link = data[:links].find {|l| (l[:rel] == "lrdd" && l[:type] == "application/xrd+xml") }
|
95
|
+
return link[:template] unless link.nil?
|
96
|
+
end
|
97
|
+
private_class_method :webfinger_url_from_xrd
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,263 @@
|
|
1
|
+
module DiasporaFederation
|
2
|
+
module WebFinger
|
3
|
+
##
|
4
|
+
# The WebFinger document used for Diaspora* user discovery is based on an older
|
5
|
+
# draft of the specification you can find in the wiki of the "webfinger" project
|
6
|
+
# on {http://code.google.com/p/webfinger/wiki/WebFingerProtocol Google Code}
|
7
|
+
# (from around 2010).
|
8
|
+
#
|
9
|
+
# In the meantime an actual RFC draft has been in development, which should
|
10
|
+
# serve as a base for all future changes of this implementation.
|
11
|
+
#
|
12
|
+
# @example Creating a WebFinger document from account data
|
13
|
+
# wf = WebFinger.from_person({
|
14
|
+
# acct_uri: "acct:user@server.example",
|
15
|
+
# alias_url: "https://server.example/people/0123456789abcdef",
|
16
|
+
# hcard_url: "https://server.example/hcard/users/0123456789abcdef",
|
17
|
+
# seed_url: "https://server.example/",
|
18
|
+
# profile_url: "https://server.example/u/user",
|
19
|
+
# atom_url: "https://server.example/public/user.atom",
|
20
|
+
# salmon_url: "https://server.example/receive/users/0123456789abcdef",
|
21
|
+
# guid: "0123456789abcdef",
|
22
|
+
# pubkey: "-----BEGIN PUBLIC KEY-----\nABCDEF==\n-----END PUBLIC KEY-----"
|
23
|
+
# })
|
24
|
+
# xml_string = wf.to_xml
|
25
|
+
#
|
26
|
+
# @example Creating a WebFinger instance from an xml document
|
27
|
+
# wf = WebFinger.from_xml(xml_string)
|
28
|
+
# ...
|
29
|
+
# hcard_url = wf.hcard_url
|
30
|
+
# ...
|
31
|
+
#
|
32
|
+
# @see http://tools.ietf.org/html/draft-jones-appsawg-webfinger "WebFinger" -
|
33
|
+
# current draft
|
34
|
+
# @see http://code.google.com/p/webfinger/wiki/CommonLinkRelations
|
35
|
+
# @see http://www.iana.org/assignments/link-relations/link-relations.xhtml
|
36
|
+
# official list of IANA link relations
|
37
|
+
class WebFinger
|
38
|
+
private_class_method :new
|
39
|
+
|
40
|
+
# The Subject element should contain the webfinger address that was asked
|
41
|
+
# for. If it does not, then this webfinger profile MUST be ignored.
|
42
|
+
# @return [String]
|
43
|
+
attr_reader :acct_uri
|
44
|
+
|
45
|
+
# @return [String] link to the users profile
|
46
|
+
attr_reader :alias_url, :profile_url
|
47
|
+
|
48
|
+
# @return [String] link to the +hCard+
|
49
|
+
attr_reader :hcard_url
|
50
|
+
|
51
|
+
# @return [String] link to the pod
|
52
|
+
attr_reader :seed_url
|
53
|
+
|
54
|
+
# This atom feed is an Activity Stream of the user's public posts. Diaspora
|
55
|
+
# pods SHOULD publish an Activity Stream of public posts, but there is
|
56
|
+
# currently no requirement to be able to read Activity Streams.
|
57
|
+
# @see http://activitystrea.ms/ Activity Streams specification
|
58
|
+
#
|
59
|
+
# Note that this feed MAY also be made available through the PubSubHubbub
|
60
|
+
# mechanism by supplying a <link rel="hub"> in the atom feed itself.
|
61
|
+
# @return [String] atom feed url
|
62
|
+
attr_reader :atom_url
|
63
|
+
|
64
|
+
# @return [String] salmon endpoint url
|
65
|
+
# @see http://salmon-protocol.googlecode.com/svn/trunk/draft-panzer-salmon-00.html#SMLR
|
66
|
+
# Panzer draft for Salmon, paragraph 3.3
|
67
|
+
attr_reader :salmon_url
|
68
|
+
|
69
|
+
# @deprecated Either convert these to +Property+ elements or move to the
|
70
|
+
# +hCard+, which actually has fields for an +UID+ defined in the +vCard+
|
71
|
+
# specification (will affect older Diaspora* installations).
|
72
|
+
#
|
73
|
+
# @see HCard#guid
|
74
|
+
#
|
75
|
+
# This is just the guid. When a user creates an account on a pod, the pod
|
76
|
+
# MUST assign them a guid - a random hexadecimal string of at least 8
|
77
|
+
# hexadecimal digits.
|
78
|
+
# @return [String] guid
|
79
|
+
attr_reader :guid
|
80
|
+
|
81
|
+
# @deprecated Either convert these to +Property+ elements or move to the
|
82
|
+
# +hCard+, which actually has fields for an +KEY+ defined in the +vCard+
|
83
|
+
# specification (will affect older Diaspora* installations).
|
84
|
+
#
|
85
|
+
# @see HCard#pubkey
|
86
|
+
#
|
87
|
+
# When a user is created on the pod, the pod MUST generate a pgp keypair
|
88
|
+
# for them. This key is used for signing messages. The format is a
|
89
|
+
# DER-encoded PKCS#1 key beginning with the text
|
90
|
+
# "-----BEGIN PUBLIC KEY-----" and ending with "-----END PUBLIC KEY-----".
|
91
|
+
# @return [String] public key
|
92
|
+
attr_reader :pubkey
|
93
|
+
|
94
|
+
# +hcard_url+ link relation
|
95
|
+
REL_HCARD = "http://microformats.org/profile/hcard"
|
96
|
+
|
97
|
+
# +seed_url+ link relation
|
98
|
+
REL_SEED = "http://joindiaspora.com/seed_location"
|
99
|
+
|
100
|
+
# @deprecated This should be a +Property+ or moved to the +hCard+, but +Link+
|
101
|
+
# is inappropriate according to the specification (will affect older
|
102
|
+
# Diaspora* installations).
|
103
|
+
# +guid+ link relation
|
104
|
+
REL_GUID = "http://joindiaspora.com/guid"
|
105
|
+
|
106
|
+
# +profile_url+ link relation.
|
107
|
+
# @note This might just as well be an +Alias+ instead of a +Link+.
|
108
|
+
REL_PROFILE = "http://webfinger.net/rel/profile-page"
|
109
|
+
|
110
|
+
# +atom_url+ link relation
|
111
|
+
REL_ATOM = "http://schemas.google.com/g/2010#updates-from"
|
112
|
+
|
113
|
+
# +salmon_url+ link relation
|
114
|
+
REL_SALMON = "salmon"
|
115
|
+
|
116
|
+
# @deprecated This should be a +Property+ or moved to the +hcard+, but +Link+
|
117
|
+
# is inappropriate according to the specification (will affect older
|
118
|
+
# Diaspora* installations).
|
119
|
+
# +pubkey+ link relation
|
120
|
+
REL_PUBKEY = "diaspora-public-key"
|
121
|
+
|
122
|
+
# Create the XML string from the current WebFinger instance
|
123
|
+
# @return [String] XML string
|
124
|
+
def to_xml
|
125
|
+
doc = XrdDocument.new
|
126
|
+
doc.subject = @acct_uri
|
127
|
+
doc.aliases << @alias_url
|
128
|
+
|
129
|
+
add_links_to(doc)
|
130
|
+
|
131
|
+
doc.to_xml
|
132
|
+
end
|
133
|
+
|
134
|
+
# Create a WebFinger instance from the given person data Hash.
|
135
|
+
# @param [Hash] data account data
|
136
|
+
# @return [WebFinger] WebFinger instance
|
137
|
+
# @raise [InvalidData] if the given data Hash is invalid or incomplete
|
138
|
+
def self.from_person(data)
|
139
|
+
raise InvalidData, "person data incomplete" unless account_data_complete?(data)
|
140
|
+
|
141
|
+
wf = allocate
|
142
|
+
wf.instance_eval {
|
143
|
+
@acct_uri = data[:acct_uri]
|
144
|
+
@alias_url = data[:alias_url]
|
145
|
+
@hcard_url = data[:hcard_url]
|
146
|
+
@seed_url = data[:seed_url]
|
147
|
+
@profile_url = data[:profile_url]
|
148
|
+
@atom_url = data[:atom_url]
|
149
|
+
@salmon_url = data[:salmon_url]
|
150
|
+
|
151
|
+
# TODO: remove me! #########
|
152
|
+
@guid = data[:guid]
|
153
|
+
@pubkey = data[:pubkey]
|
154
|
+
#############################
|
155
|
+
}
|
156
|
+
wf
|
157
|
+
end
|
158
|
+
|
159
|
+
# Create a WebFinger instance from the given XML string.
|
160
|
+
# @param [String] webfinger_xml WebFinger XML string
|
161
|
+
# @return [WebFinger] WebFinger instance
|
162
|
+
# @raise [InvalidData] if the given XML string is invalid or incomplete
|
163
|
+
def self.from_xml(webfinger_xml)
|
164
|
+
data = parse_xml_and_validate(webfinger_xml)
|
165
|
+
|
166
|
+
hcard_url, seed_url, guid, profile_url, atom_url, salmon_url, pubkey = parse_links(data)
|
167
|
+
|
168
|
+
wf = allocate
|
169
|
+
wf.instance_eval {
|
170
|
+
@acct_uri = data[:subject]
|
171
|
+
@alias_url = data[:aliases].first
|
172
|
+
@hcard_url = hcard_url
|
173
|
+
@seed_url = seed_url
|
174
|
+
@profile_url = profile_url
|
175
|
+
@atom_url = atom_url
|
176
|
+
@salmon_url = salmon_url
|
177
|
+
|
178
|
+
# TODO: remove me! ##########
|
179
|
+
@guid = guid
|
180
|
+
@pubkey = Base64.strict_decode64(pubkey)
|
181
|
+
##############################
|
182
|
+
}
|
183
|
+
wf
|
184
|
+
end
|
185
|
+
|
186
|
+
private
|
187
|
+
|
188
|
+
# Checks the given account data Hash for correct type and completeness.
|
189
|
+
# @param [Hash] data account data
|
190
|
+
# @return [Boolean] validation result
|
191
|
+
def self.account_data_complete?(data)
|
192
|
+
data.instance_of?(Hash) &&
|
193
|
+
%i(
|
194
|
+
acct_uri alias_url hcard_url seed_url
|
195
|
+
guid profile_url atom_url salmon_url pubkey
|
196
|
+
).all? {|k| data.key? k }
|
197
|
+
end
|
198
|
+
private_class_method :account_data_complete?
|
199
|
+
|
200
|
+
# Parses the XML string to a Hash and does some rudimentary checking on
|
201
|
+
# the data Hash.
|
202
|
+
# @param [String] webfinger_xml WebFinger XML string
|
203
|
+
# @return [Hash] data XML data
|
204
|
+
# @raise [InvalidData] if the given XML string is invalid or incomplete
|
205
|
+
def self.parse_xml_and_validate(webfinger_xml)
|
206
|
+
data = XrdDocument.xml_data(webfinger_xml)
|
207
|
+
valid = data.key?(:subject) && data.key?(:aliases) && data.key?(:links)
|
208
|
+
raise InvalidData, "webfinger xml is incomplete" unless valid
|
209
|
+
data
|
210
|
+
end
|
211
|
+
private_class_method :parse_xml_and_validate
|
212
|
+
|
213
|
+
def add_links_to(doc)
|
214
|
+
doc.links << {rel: REL_HCARD,
|
215
|
+
type: "text/html",
|
216
|
+
href: @hcard_url}
|
217
|
+
doc.links << {rel: REL_SEED,
|
218
|
+
type: "text/html",
|
219
|
+
href: @seed_url}
|
220
|
+
|
221
|
+
# TODO: remove me! ##############
|
222
|
+
doc.links << {rel: REL_GUID,
|
223
|
+
type: "text/html",
|
224
|
+
href: @guid}
|
225
|
+
##################################
|
226
|
+
|
227
|
+
doc.links << {rel: REL_PROFILE,
|
228
|
+
type: "text/html",
|
229
|
+
href: @profile_url}
|
230
|
+
doc.links << {rel: REL_ATOM,
|
231
|
+
type: "application/atom+xml",
|
232
|
+
href: @atom_url}
|
233
|
+
doc.links << {rel: REL_SALMON,
|
234
|
+
href: @salmon_url}
|
235
|
+
|
236
|
+
# TODO: remove me! ##############
|
237
|
+
doc.links << {rel: REL_PUBKEY,
|
238
|
+
type: "RSA",
|
239
|
+
href: Base64.strict_encode64(@pubkey)}
|
240
|
+
##################################
|
241
|
+
end
|
242
|
+
|
243
|
+
def self.parse_links(data)
|
244
|
+
links = data[:links]
|
245
|
+
hcard = parse_link(links, REL_HCARD)
|
246
|
+
seed = parse_link(links, REL_SEED)
|
247
|
+
guid = parse_link(links, REL_GUID)
|
248
|
+
profile = parse_link(links, REL_PROFILE)
|
249
|
+
atom = parse_link(links, REL_ATOM)
|
250
|
+
salmon = parse_link(links, REL_SALMON)
|
251
|
+
pubkey = parse_link(links, REL_PUBKEY)
|
252
|
+
raise InvalidData, "webfinger xml is incomplete" unless [hcard, seed, guid, profile, atom, salmon, pubkey].all?
|
253
|
+
[hcard[:href], seed[:href], guid[:href], profile[:href], atom[:href], salmon[:href], pubkey[:href]]
|
254
|
+
end
|
255
|
+
private_class_method :parse_links
|
256
|
+
|
257
|
+
def self.parse_link(links, rel)
|
258
|
+
links.find {|l| l[:rel] == rel }
|
259
|
+
end
|
260
|
+
private_class_method :parse_link
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
module DiasporaFederation
|
2
|
+
module WebFinger
|
3
|
+
##
|
4
|
+
# This class implements basic handling of XRD documents as far as it is
|
5
|
+
# necessary in the context of the protocols used with Diaspora* federation.
|
6
|
+
#
|
7
|
+
# @note {http://tools.ietf.org/html/rfc6415 RFC 6415} recommends that servers
|
8
|
+
# should also offer the JRD format in addition to the XRD representation.
|
9
|
+
# Implementing +XrdDocument#to_json+ and +XrdDocument.json_data+ should
|
10
|
+
# be almost trivial due to the simplicity of the format and the way the data
|
11
|
+
# is stored internally already. See
|
12
|
+
# {http://tools.ietf.org/html/rfc6415#appendix-A RFC 6415, Appendix A}
|
13
|
+
# for a description of the JSON format.
|
14
|
+
#
|
15
|
+
# @example Creating a XrdDocument
|
16
|
+
# doc = XrdDocument.new
|
17
|
+
# doc.expires = DateTime.new(2020, 1, 15, 0, 0, 1)
|
18
|
+
# doc.subject = "http://example.tld/articles/11"
|
19
|
+
# doc.aliases << "http://example.tld/cool_article"
|
20
|
+
# doc.aliases << "http://example.tld/authors/2/articles/3"
|
21
|
+
# doc.properties["http://x.example.tld/ns/version"] = "1.3"
|
22
|
+
# doc.links << { rel: "author", type: "text/html", href: "http://example.tld/authors/2" }
|
23
|
+
# doc.links << { rel: "copyright", template: "http://example.tld/copyright?id={uri}" }
|
24
|
+
#
|
25
|
+
# doc.to_xml
|
26
|
+
#
|
27
|
+
# @example Parsing a XrdDocument
|
28
|
+
# data = XrdDocument.xml_data(xml_string)
|
29
|
+
#
|
30
|
+
# @see http://docs.oasis-open.org/xri/xrd/v1.0/xrd-1.0.html Extensible Resource Descriptor (XRD) Version 1.0
|
31
|
+
class XrdDocument
|
32
|
+
# xml namespace url
|
33
|
+
XMLNS = "http://docs.oasis-open.org/ns/xri/xrd-1.0"
|
34
|
+
|
35
|
+
# +Link+ element attributes
|
36
|
+
LINK_ATTRS = %i(rel type href template)
|
37
|
+
|
38
|
+
# format string for datetime (+Expires+ element)
|
39
|
+
DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
|
40
|
+
|
41
|
+
# The <Expires> element contains a time value which specifies the instant at
|
42
|
+
# and after which the document has expired and SHOULD NOT be used.
|
43
|
+
# @param [DateTime] value
|
44
|
+
attr_writer :expires
|
45
|
+
# The <Subject> element contains a URI value which identifies the resource
|
46
|
+
# described by this XRD.
|
47
|
+
# @param [String] value
|
48
|
+
attr_writer :subject
|
49
|
+
|
50
|
+
# @return [Array<String>] list of alias URIs
|
51
|
+
attr_reader :aliases
|
52
|
+
|
53
|
+
# @return [Hash<String => mixed>] list of properties. Hash key represents the
|
54
|
+
# +type+ attribute, and the value is the element content
|
55
|
+
attr_reader :properties
|
56
|
+
|
57
|
+
# @return [Array<Hash<attr => val>>] list of +Link+ element hashes. Each
|
58
|
+
# hash contains the attributesa and their associated values for the +Link+
|
59
|
+
# element.
|
60
|
+
attr_reader :links
|
61
|
+
|
62
|
+
def initialize
|
63
|
+
@aliases = []
|
64
|
+
@links = []
|
65
|
+
@properties = {}
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# Generates an XML document from the current instance and returns it as string
|
70
|
+
# @return [String] XML document
|
71
|
+
def to_xml
|
72
|
+
builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
|
73
|
+
xml.XRD("xmlns" => XMLNS) {
|
74
|
+
xml.Expires(@expires.strftime(DATETIME_FORMAT)) if @expires.instance_of?(DateTime)
|
75
|
+
|
76
|
+
xml.Subject(@subject) if !@subject.nil? && !@subject.empty?
|
77
|
+
|
78
|
+
add_aliases_to(xml)
|
79
|
+
add_properties_to(xml)
|
80
|
+
add_links_to(xml)
|
81
|
+
}
|
82
|
+
end
|
83
|
+
builder.to_xml
|
84
|
+
end
|
85
|
+
|
86
|
+
##
|
87
|
+
# Parse the XRD document from the given string and create a hash containing
|
88
|
+
# the extracted data.
|
89
|
+
#
|
90
|
+
# Small bonus: the hash structure that comes out of this method is the same
|
91
|
+
# as the one used to produce a JRD (JSON Resource Descriptor) or parsing it.
|
92
|
+
#
|
93
|
+
# @param [String] xrd_doc XML string
|
94
|
+
# @return [Hash] extracted data
|
95
|
+
# @raise [InvalidDocument] if the XRD is malformed
|
96
|
+
def self.xml_data(xrd_doc)
|
97
|
+
doc = parse_xrd_document(xrd_doc)
|
98
|
+
data = {}
|
99
|
+
|
100
|
+
exp_elem = doc.at_xpath("xrd:XRD/xrd:Expires", NS)
|
101
|
+
data[:expires] = DateTime.strptime(exp_elem.content, DATETIME_FORMAT) unless exp_elem.nil?
|
102
|
+
|
103
|
+
subj_elem = doc.at_xpath("xrd:XRD/xrd:Subject", NS)
|
104
|
+
data[:subject] = subj_elem.content unless subj_elem.nil?
|
105
|
+
|
106
|
+
parse_aliases_from_xml_doc(doc, data)
|
107
|
+
parse_properties_from_xml_doc(doc, data)
|
108
|
+
parse_links_from_xml_doc(doc, data)
|
109
|
+
|
110
|
+
data
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
NS = {xrd: XMLNS}
|
116
|
+
|
117
|
+
def add_aliases_to(xml)
|
118
|
+
@aliases.each do |a|
|
119
|
+
next if !a.instance_of?(String) || a.empty?
|
120
|
+
xml.Alias(a.to_s)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def add_properties_to(xml)
|
125
|
+
@properties.each do |type, val|
|
126
|
+
xml.Property(val.to_s, type: type)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def add_links_to(xml)
|
131
|
+
@links.each do |l|
|
132
|
+
attrs = {}
|
133
|
+
LINK_ATTRS.each do |attr|
|
134
|
+
attrs[attr.to_s] = l[attr] if l.key?(attr)
|
135
|
+
end
|
136
|
+
xml.Link(attrs)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.parse_xrd_document(xrd_doc)
|
141
|
+
raise ArgumentError unless xrd_doc.instance_of?(String)
|
142
|
+
|
143
|
+
doc = Nokogiri::XML::Document.parse(xrd_doc)
|
144
|
+
raise InvalidDocument, "Not an XRD document" if !doc.root || doc.root.name != "XRD"
|
145
|
+
doc
|
146
|
+
end
|
147
|
+
private_class_method :parse_xrd_document
|
148
|
+
|
149
|
+
def self.parse_aliases_from_xml_doc(doc, data)
|
150
|
+
aliases = []
|
151
|
+
doc.xpath("xrd:XRD/xrd:Alias", NS).each do |node|
|
152
|
+
aliases << node.content
|
153
|
+
end
|
154
|
+
data[:aliases] = aliases unless aliases.empty?
|
155
|
+
end
|
156
|
+
private_class_method :parse_aliases_from_xml_doc
|
157
|
+
|
158
|
+
def self.parse_properties_from_xml_doc(doc, data)
|
159
|
+
properties = {}
|
160
|
+
doc.xpath("xrd:XRD/xrd:Property", NS).each do |node|
|
161
|
+
properties[node[:type]] = node.children.empty? ? nil : node.content
|
162
|
+
end
|
163
|
+
data[:properties] = properties unless properties.empty?
|
164
|
+
end
|
165
|
+
private_class_method :parse_properties_from_xml_doc
|
166
|
+
|
167
|
+
def self.parse_links_from_xml_doc(doc, data)
|
168
|
+
links = []
|
169
|
+
doc.xpath("xrd:XRD/xrd:Link", NS).each do |node|
|
170
|
+
link = {}
|
171
|
+
LINK_ATTRS.each do |attr|
|
172
|
+
link[attr] = node[attr.to_s] if node.key?(attr.to_s)
|
173
|
+
end
|
174
|
+
links << link
|
175
|
+
end
|
176
|
+
data[:links] = links unless links.empty?
|
177
|
+
end
|
178
|
+
private_class_method :parse_links_from_xml_doc
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|