diaspora_federation 0.0.1

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