diaspora_federation 0.0.13 → 0.1.0

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