diaspora_federation 0.0.13 → 0.1.0

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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -32
  3. data/lib/diaspora_federation.rb +9 -31
  4. data/lib/diaspora_federation/discovery/h_card.rb +8 -15
  5. data/lib/diaspora_federation/discovery/host_meta.rb +2 -4
  6. data/lib/diaspora_federation/discovery/web_finger.rb +11 -11
  7. data/lib/diaspora_federation/discovery/xrd_document.rb +4 -8
  8. data/lib/diaspora_federation/entities.rb +2 -0
  9. data/lib/diaspora_federation/entities/account_deletion.rb +5 -0
  10. data/lib/diaspora_federation/entities/comment.rb +3 -6
  11. data/lib/diaspora_federation/entities/contact.rb +5 -0
  12. data/lib/diaspora_federation/entities/conversation.rb +10 -1
  13. data/lib/diaspora_federation/entities/message.rb +29 -4
  14. data/lib/diaspora_federation/entities/participation.rb +24 -0
  15. data/lib/diaspora_federation/entities/poll_participation.rb +3 -6
  16. data/lib/diaspora_federation/entities/profile.rb +5 -0
  17. data/lib/diaspora_federation/entities/related_entity.rb +33 -0
  18. data/lib/diaspora_federation/entities/relayable.rb +55 -40
  19. data/lib/diaspora_federation/entities/relayable_retraction.rb +21 -12
  20. data/lib/diaspora_federation/entities/request.rb +6 -2
  21. data/lib/diaspora_federation/entities/reshare.rb +5 -0
  22. data/lib/diaspora_federation/entities/retraction.rb +37 -0
  23. data/lib/diaspora_federation/entities/signed_retraction.rb +16 -5
  24. data/lib/diaspora_federation/entities/status_message.rb +11 -0
  25. data/lib/diaspora_federation/entity.rb +73 -30
  26. data/lib/diaspora_federation/federation/fetcher.rb +11 -1
  27. data/lib/diaspora_federation/federation/receiver.rb +10 -0
  28. data/lib/diaspora_federation/federation/receiver/abstract_receiver.rb +18 -4
  29. data/lib/diaspora_federation/federation/receiver/exceptions.rb +4 -0
  30. data/lib/diaspora_federation/federation/receiver/public.rb +10 -0
  31. data/lib/diaspora_federation/federation/sender.rb +1 -1
  32. data/lib/diaspora_federation/http_client.rb +1 -2
  33. data/lib/diaspora_federation/logging.rb +6 -0
  34. data/lib/diaspora_federation/properties_dsl.rb +4 -2
  35. data/lib/diaspora_federation/salmon/encrypted_magic_envelope.rb +2 -2
  36. data/lib/diaspora_federation/salmon/encrypted_slap.rb +3 -5
  37. data/lib/diaspora_federation/salmon/exceptions.rb +1 -1
  38. data/lib/diaspora_federation/salmon/magic_envelope.rb +16 -17
  39. data/lib/diaspora_federation/salmon/slap.rb +1 -2
  40. data/lib/diaspora_federation/salmon/xml_payload.rb +1 -2
  41. data/lib/diaspora_federation/validators.rb +2 -0
  42. data/lib/diaspora_federation/validators/conversation_validator.rb +2 -0
  43. data/lib/diaspora_federation/validators/message_validator.rb +2 -2
  44. data/lib/diaspora_federation/validators/participation_validator.rb +3 -2
  45. data/lib/diaspora_federation/validators/poll_validator.rb +1 -0
  46. data/lib/diaspora_federation/validators/related_entity_validator.rb +12 -0
  47. data/lib/diaspora_federation/validators/relayable_retraction_validator.rb +1 -1
  48. data/lib/diaspora_federation/validators/relayable_validator.rb +1 -0
  49. data/lib/diaspora_federation/validators/retraction_validator.rb +1 -1
  50. data/lib/diaspora_federation/validators/rules/diaspora_id.rb +8 -11
  51. data/lib/diaspora_federation/validators/rules/diaspora_id_count.rb +1 -1
  52. data/lib/diaspora_federation/validators/signed_retraction_validator.rb +1 -1
  53. data/lib/diaspora_federation/validators/status_message_validator.rb +2 -0
  54. data/lib/diaspora_federation/validators/web_finger_validator.rb +2 -2
  55. data/lib/diaspora_federation/version.rb +1 -1
  56. metadata +9 -7
@@ -30,10 +30,15 @@ module DiasporaFederation
30
30
  # @return [String] author signature
31
31
  property :target_author_signature, default: nil
32
32
 
33
+ # @!attribute [r] target
34
+ # target entity
35
+ # @return [RelatedEntity] target entity
36
+ entity :target, Entities::RelatedEntity
37
+
33
38
  # use only {Retraction} for receive
34
39
  # @return [Retraction] instance as normal retraction
35
40
  def to_retraction
36
- Retraction.new(author: author, target_guid: target_guid, target_type: target_type)
41
+ Retraction.new(author: author, target_guid: target_guid, target_type: target_type, target: target)
37
42
  end
38
43
 
39
44
  # Create signature for a retraction
@@ -44,14 +49,20 @@ module DiasporaFederation
44
49
  Base64.strict_encode64(privkey.sign(Relayable::DIGEST, [ret.target_guid, ret.target_type].join(";")))
45
50
  end
46
51
 
52
+ # @return [String] string representation of this object
53
+ def to_s
54
+ "SignedRetraction:#{target_type}:#{target_guid}"
55
+ end
56
+
47
57
  private
48
58
 
49
59
  # @param [Nokogiri::XML::Element] root_node xml nodes
50
60
  # @return [Retraction] instance
51
- def self.populate_entity(root_node)
52
- super(root_node).to_retraction
61
+ private_class_method def self.populate_entity(root_node)
62
+ entity_data = entity_data(root_node)
63
+ entity_data[:target] = Retraction.send(:fetch_target, entity_data[:target_type], entity_data[:target_guid])
64
+ new(entity_data).to_retraction
53
65
  end
54
- private_class_method :populate_entity
55
66
 
56
67
  # It updates also the signatures with the keys of the author and the parent
57
68
  # if the signatures are not there yet and if the keys are available.
@@ -64,7 +75,7 @@ module DiasporaFederation
64
75
  end
65
76
 
66
77
  def sign_with_author
67
- privkey = DiasporaFederation.callbacks.trigger(:fetch_private_key_by_diaspora_id, author)
78
+ privkey = DiasporaFederation.callbacks.trigger(:fetch_private_key, author)
68
79
  SignedRetraction.sign_with_key(privkey, self) unless privkey.nil?
69
80
  end
70
81
  end
@@ -30,6 +30,17 @@ module DiasporaFederation
30
30
  # shows whether the status message is visible to everyone or only to some aspects
31
31
  # @return [Boolean] is it public
32
32
  property :public, default: false
33
+
34
+ private
35
+
36
+ def validate
37
+ super
38
+ photos.each do |photo|
39
+ if photo.author != author
40
+ raise ValidationError, "nested #{photo} has different author: author=#{author} obj=#{self}"
41
+ end
42
+ end
43
+ end
33
44
  end
34
45
  end
35
46
  end
@@ -34,6 +34,11 @@ module DiasporaFederation
34
34
  # are intended to be immutable data containers, only.
35
35
  class Entity
36
36
  extend PropertiesDSL
37
+ include Logging
38
+
39
+ # Invalid XML characters
40
+ # @see https://www.w3.org/TR/REC-xml/#charsets "Extensible Markup Language (XML) 1.0"
41
+ INVALID_XML_REGEX = /[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]/
37
42
 
38
43
  # Initializes the Entity with the given attribute hash and freezes the created
39
44
  # instance it returns.
@@ -50,27 +55,35 @@ module DiasporaFederation
50
55
  # @param [Hash] data entity data
51
56
  # @return [Entity] new instance
52
57
  def initialize(data)
58
+ logger.debug "create entity #{self.class} with data: #{data}"
53
59
  raise ArgumentError, "expected a Hash" unless data.is_a?(Hash)
60
+
54
61
  entity_data = self.class.resolv_aliases(data)
55
- missing_props = self.class.missing_props(entity_data)
56
- unless missing_props.empty?
57
- raise ArgumentError, "missing required properties: #{missing_props.join(', ')}"
58
- end
62
+ validate_missing_props(entity_data)
59
63
 
60
64
  self.class.default_values.merge(entity_data).each do |name, value|
61
- instance_variable_set("@#{name}", nilify(value)) if setable?(name, value)
65
+ instance_variable_set("@#{name}", instantiate_nested(name, nilify(value))) if setable?(name, value)
62
66
  end
63
67
 
64
68
  freeze
65
69
  validate
66
70
  end
67
71
 
68
- # Returns a Hash representing this Entity (attributes => values)
72
+ # Returns a Hash representing this Entity (attributes => values).
73
+ # Nested entities are also converted to a Hash.
69
74
  # @return [Hash] entity data (mostly equal to the hash used for initialization).
70
75
  def to_h
71
- self.class.class_props.keys.each_with_object({}) do |prop, hash|
72
- hash[prop] = public_send(prop)
73
- end
76
+ properties.map {|key, value|
77
+ type = self.class.class_props[key]
78
+
79
+ if type == String || value.nil?
80
+ [key, value]
81
+ elsif type.instance_of?(Class)
82
+ [key, value.to_h]
83
+ elsif type.instance_of?(Array)
84
+ [key, value.map(&:to_h)]
85
+ end
86
+ }.to_h
74
87
  end
75
88
 
76
89
  # Returns the XML representation for this entity constructed out of
@@ -131,8 +144,18 @@ module DiasporaFederation
131
144
  Entities.const_get(class_name)
132
145
  end
133
146
 
147
+ # @return [String] string representation of this object
148
+ def to_s
149
+ "#{self.class.name.rpartition('::').last}#{":#{guid}" if respond_to?(:guid)}"
150
+ end
151
+
134
152
  private
135
153
 
154
+ def validate_missing_props(entity_data)
155
+ missing_props = self.class.missing_props(entity_data)
156
+ raise ArgumentError, "missing required properties: #{missing_props.join(', ')}" unless missing_props.empty?
157
+ end
158
+
136
159
  def setable?(name, val)
137
160
  type = self.class.class_props[name]
138
161
  return false if type.nil? # property undefined
@@ -145,18 +168,30 @@ module DiasporaFederation
145
168
  end
146
169
 
147
170
  def setable_nested?(type, val)
148
- type.is_a?(Class) && type.ancestors.include?(Entity) && val.is_a?(Entity)
171
+ type.instance_of?(Class) && type.ancestors.include?(Entity) && (val.is_a?(Entity) || val.is_a?(Hash))
149
172
  end
150
173
 
151
174
  def setable_multi?(type, val)
152
- type.instance_of?(Array) && val.instance_of?(Array) && val.all? {|v| v.instance_of?(type.first) }
175
+ type.instance_of?(Array) && val.instance_of?(Array) &&
176
+ (val.all? {|v| v.instance_of?(type.first) } || val.all? {|v| v.instance_of?(Hash) })
153
177
  end
154
178
 
155
179
  def nilify(value)
156
- return nil if value.respond_to?(:empty?) && value.empty?
180
+ return nil if value.respond_to?(:empty?) && value.empty? && !value.instance_of?(Array)
157
181
  value
158
182
  end
159
183
 
184
+ def instantiate_nested(name, value)
185
+ if value.instance_of?(Array)
186
+ return value unless value.first.instance_of?(Hash)
187
+ value.map {|hash| self.class.class_props[name].first.new(hash) }
188
+ elsif value.instance_of?(Hash)
189
+ self.class.class_props[name].new(value)
190
+ else
191
+ value
192
+ end
193
+ end
194
+
160
195
  def validate
161
196
  validator_name = "#{self.class.name.split('::').last}Validator"
162
197
  return unless Validators.const_defined? validator_name
@@ -173,8 +208,15 @@ module DiasporaFederation
173
208
  "Failed validation for properties: #{errors.join(' | ')}"
174
209
  end
175
210
 
211
+ # @return [Hash] hash with all properties
212
+ def properties
213
+ self.class.class_props.keys.each_with_object({}) do |prop, hash|
214
+ hash[prop] = public_send(prop)
215
+ end
216
+ end
217
+
176
218
  def xml_elements
177
- Hash[to_h.map {|name, value| [name, self.class.class_props[name] == String ? value.to_s : value] }]
219
+ properties.map {|name, value| [name, self.class.class_props[name] == String ? value.to_s : value] }.to_h
178
220
  end
179
221
 
180
222
  def add_property_to_xml(doc, root_element, name, value)
@@ -183,7 +225,8 @@ module DiasporaFederation
183
225
  else
184
226
  # call #to_xml for each item and append to root
185
227
  [*value].compact.each do |item|
186
- root_element << item.to_xml
228
+ child = item.to_xml
229
+ root_element << child if child
187
230
  end
188
231
  end
189
232
  end
@@ -192,26 +235,30 @@ module DiasporaFederation
192
235
  def simple_node(doc, name, value)
193
236
  xml_name = self.class.xml_names[name]
194
237
  Nokogiri::XML::Element.new(xml_name ? xml_name.to_s : name, doc).tap do |node|
195
- node.content = value unless value.empty?
238
+ node.content = value.gsub(INVALID_XML_REGEX, "\uFFFD") unless value.empty?
196
239
  end
197
240
  end
198
241
 
199
242
  # @param [Nokogiri::XML::Element] root_node xml nodes
200
243
  # @return [Entity] instance
201
- def self.populate_entity(root_node)
202
- entity_data = Hash[class_props.map {|name, type|
203
- [name, parse_element_from_node(name, type, root_node)]
204
- }]
244
+ private_class_method def self.populate_entity(root_node)
245
+ new(entity_data(root_node))
246
+ end
205
247
 
206
- new(entity_data)
248
+ # @param [Nokogiri::XML::Element] root_node xml nodes
249
+ # @return [Hash] entity data
250
+ private_class_method def self.entity_data(root_node)
251
+ class_props.map {|name, type|
252
+ value = parse_element_from_node(name, type, root_node)
253
+ [name, value] if value
254
+ }.compact.to_h
207
255
  end
208
- private_class_method :populate_entity
209
256
 
210
257
  # @param [String] name property name to parse
211
258
  # @param [Class] type target type to parse
212
259
  # @param [Nokogiri::XML::Element] root_node XML node to parse
213
260
  # @return [Object] parsed data
214
- def self.parse_element_from_node(name, type, root_node)
261
+ private_class_method def self.parse_element_from_node(name, type, root_node)
215
262
  if type == String
216
263
  parse_string_from_node(name, root_node)
217
264
  elsif type.instance_of?(Array)
@@ -220,41 +267,37 @@ module DiasporaFederation
220
267
  parse_entity_from_node(type, root_node)
221
268
  end
222
269
  end
223
- private_class_method :parse_element_from_node
224
270
 
225
271
  # create simple entry in data hash
226
272
  #
227
273
  # @param [String] name xml tag to parse
228
274
  # @param [Nokogiri::XML::Element] root_node XML root_node to parse
229
275
  # @return [String] data
230
- def self.parse_string_from_node(name, root_node)
276
+ private_class_method def self.parse_string_from_node(name, root_node)
231
277
  node = root_node.xpath(name.to_s)
232
278
  node = root_node.xpath(xml_names[name].to_s) if node.empty?
233
279
  node.first.text if node.any?
234
280
  end
235
- private_class_method :parse_string_from_node
236
281
 
237
282
  # create an entry in the data hash for the nested entity
238
283
  #
239
284
  # @param [Class] type target type to parse
240
285
  # @param [Nokogiri::XML::Element] root_node XML node to parse
241
286
  # @return [Entity] parsed child entity
242
- def self.parse_entity_from_node(type, root_node)
287
+ private_class_method def self.parse_entity_from_node(type, root_node)
243
288
  node = root_node.xpath(type.entity_name)
244
289
  type.from_xml(node.first) if node.any?
245
290
  end
246
- private_class_method :parse_entity_from_node
247
291
 
248
292
  # collect all nested children of that type and create an array in the data hash
249
293
  #
250
294
  # @param [Class] type target type to parse
251
295
  # @param [Nokogiri::XML::Element] root_node XML node to parse
252
296
  # @return [Array<Entity>] array with parsed child entities
253
- def self.parse_array_from_node(type, root_node)
297
+ private_class_method def self.parse_array_from_node(type, root_node)
254
298
  node = root_node.xpath(type.entity_name)
255
- node.map {|child| type.from_xml(child) }
299
+ node.map {|child| type.from_xml(child) } unless node.empty?
256
300
  end
257
- private_class_method :parse_array_from_node
258
301
 
259
302
  # Raised, if entity is not valid
260
303
  class ValidationError < RuntimeError
@@ -7,7 +7,9 @@ module DiasporaFederation
7
7
  # @param [Symbol, String] entity_type snake_case version of the entity class
8
8
  # @param [String] guid guid of the entity to fetch
9
9
  def self.fetch_public(author, entity_type, guid)
10
- url = DiasporaFederation.callbacks.trigger(:fetch_person_url_to, author, "/fetch/#{entity_type}/#{guid}")
10
+ url = DiasporaFederation.callbacks.trigger(
11
+ :fetch_person_url_to, author, "/fetch/#{entity_name(entity_type)}/#{guid}"
12
+ )
11
13
  response = HttpClient.get(url)
12
14
  raise "Failed to fetch #{url}: #{response.status}" unless response.success?
13
15
 
@@ -18,6 +20,14 @@ module DiasporaFederation
18
20
  raise NotFetchable, "Failed to fetch #{entity_type}:#{guid} from #{author}: #{e.class}: #{e.message}"
19
21
  end
20
22
 
23
+ private_class_method def self.entity_name(class_name)
24
+ return class_name if class_name =~ /^[a-z]*(_[a-z]*)*$/
25
+
26
+ raise DiasporaFederation::Entity::UnknownEntity, class_name unless Entities.const_defined?(class_name)
27
+
28
+ class_name.gsub(/(.)([A-Z])/, '\1_\2').downcase
29
+ end
30
+
21
31
  # Raised, if the entity is not fetchable
22
32
  class NotFetchable < RuntimeError
23
33
  end
@@ -2,6 +2,8 @@ module DiasporaFederation
2
2
  module Federation
3
3
  # this module is for parse and receive entities.
4
4
  module Receiver
5
+ extend Logging
6
+
5
7
  # receive a public message
6
8
  # @param [String] data message to receive
7
9
  # @param [Boolean] legacy use old slap parser
@@ -13,6 +15,10 @@ module DiasporaFederation
13
15
  Salmon::MagicEnvelope.unenvelop(magic_env_xml)
14
16
  end
15
17
  Public.new(magic_env).receive
18
+ rescue => e
19
+ logger.error "failed to receive public message: #{e.class}: #{e.message}"
20
+ logger.debug "received data:\n#{data}"
21
+ raise e
16
22
  end
17
23
 
18
24
  # receive a private message
@@ -30,6 +36,10 @@ module DiasporaFederation
30
36
  Salmon::MagicEnvelope.unenvelop(magic_env_xml)
31
37
  end
32
38
  Private.new(magic_env, recipient_id).receive
39
+ rescue => e
40
+ logger.error "failed to receive private message for #{recipient_id}: #{e.class}: #{e.message}"
41
+ logger.debug "received data:\n#{data}"
42
+ raise e
33
43
  end
34
44
  end
35
45
  end
@@ -3,6 +3,8 @@ module DiasporaFederation
3
3
  module Receiver
4
4
  # common functionality for receivers
5
5
  class AbstractReceiver
6
+ include Logging
7
+
6
8
  # create a new receiver
7
9
  # @param [MagicEnvelope] magic_envelope the received magic envelope
8
10
  # @param [Object] recipient_id the identifier of the recipient of a private message
@@ -14,20 +16,32 @@ module DiasporaFederation
14
16
 
15
17
  # validate and receive the entity
16
18
  def receive
17
- validate
18
- DiasporaFederation.callbacks.trigger(:receive_entity, entity, recipient_id)
19
+ validate_and_receive
20
+ rescue => e
21
+ logger.error "failed to receive #{entity}"
22
+ raise e
19
23
  end
20
24
 
21
25
  private
22
26
 
23
27
  attr_reader :entity, :sender, :recipient_id
24
28
 
29
+ def validate_and_receive
30
+ validate
31
+ DiasporaFederation.callbacks.trigger(:receive_entity, entity, recipient_id)
32
+ logger.info "successfully received #{entity} from person #{sender}#{" for #{recipient_id}" if recipient_id}"
33
+ end
34
+
25
35
  def validate
26
- raise InvalidSender unless sender_valid?
36
+ raise InvalidSender, "invalid sender: #{sender}" unless sender_valid?
27
37
  end
28
38
 
29
39
  def sender_valid?
30
- sender == entity.author # TODO: handle sender of relayables
40
+ if entity.respond_to?(:sender_valid?)
41
+ entity.sender_valid?(sender)
42
+ else
43
+ sender == entity.author
44
+ end
31
45
  end
32
46
  end
33
47
  end
@@ -8,6 +8,10 @@ module DiasporaFederation
8
8
  # Raised, if receiving a private message without recipient.
9
9
  class RecipientRequired < RuntimeError
10
10
  end
11
+
12
+ # Raised, if receiving a message with public receiver which is not public but should be.
13
+ class NotPublic < RuntimeError
14
+ end
11
15
  end
12
16
  end
13
17
  end
@@ -3,6 +3,16 @@ module DiasporaFederation
3
3
  module Receiver
4
4
  # receiver for public entities
5
5
  class Public < AbstractReceiver
6
+ private
7
+
8
+ def validate
9
+ super
10
+ raise NotPublic if entity_can_be_public_but_it_is_not?
11
+ end
12
+
13
+ def entity_can_be_public_but_it_is_not?
14
+ entity.respond_to?(:public) && !entity.public
15
+ end
6
16
  end
7
17
  end
8
18
  end
@@ -24,7 +24,7 @@ module DiasporaFederation
24
24
  def self.private(sender_id, obj_str, targets)
25
25
  hydra = HydraWrapper.new(sender_id, obj_str)
26
26
  targets.each {|url, xml| hydra.insert_job(url, xml) }
27
- Hash[hydra.send.map {|url| [url, targets[url]] }]
27
+ hydra.send.map {|url| [url, targets[url]] }.to_h
28
28
  end
29
29
  end
30
30
  end
@@ -23,7 +23,7 @@ module DiasporaFederation
23
23
  @connection.dup
24
24
  end
25
25
 
26
- def self.create_default_connection
26
+ private_class_method def self.create_default_connection
27
27
  options = {
28
28
  request: {timeout: DiasporaFederation.http_timeout},
29
29
  ssl: {ca_file: DiasporaFederation.certificate_authorities}
@@ -36,6 +36,5 @@ module DiasporaFederation
36
36
 
37
37
  @connection.headers["User-Agent"] = DiasporaFederation.http_user_agent
38
38
  end
39
- private_class_method :create_default_connection
40
39
  end
41
40
  end
@@ -3,6 +3,12 @@ module DiasporaFederation
3
3
  #
4
4
  # it uses the logging-gem if available
5
5
  module Logging
6
+ # add +logger+ also as class method when included
7
+ # @param [Class] klass the class into which the module is included
8
+ def self.included(klass)
9
+ klass.extend(self)
10
+ end
11
+
6
12
  private
7
13
 
8
14
  # get the logger for this class