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.
@@ -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