diaspora_federation 0.0.1
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.
- 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
|