diaspora_federation-0.2.1/0000755000004100000410000000000013146064272015551 5ustar www-datawww-datadiaspora_federation-0.2.1/diaspora_federation.gemspec0000644000004100000410000001613213146064272023123 0ustar www-datawww-data######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- Gem::Specification.new do |s| s.name = "diaspora_federation" s.version = "0.2.1" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Benjamin Neff"] s.date = "2017-08-07" s.description = "This gem provides the functionality for de-/serialization and de-/encryption of Entities in the protocols used for communication among the various installations of Diaspora*" s.email = ["benjamin@coding4.coffee"] s.files = ["Changelog.md", "LICENSE", "README.md", "lib/diaspora_federation.rb", "lib/diaspora_federation/callbacks.rb", "lib/diaspora_federation/discovery.rb", "lib/diaspora_federation/discovery/discovery.rb", "lib/diaspora_federation/discovery/exceptions.rb", "lib/diaspora_federation/discovery/h_card.rb", "lib/diaspora_federation/discovery/host_meta.rb", "lib/diaspora_federation/discovery/web_finger.rb", "lib/diaspora_federation/discovery/xrd_document.rb", "lib/diaspora_federation/entities.rb", "lib/diaspora_federation/entities/account_deletion.rb", "lib/diaspora_federation/entities/account_migration.rb", "lib/diaspora_federation/entities/comment.rb", "lib/diaspora_federation/entities/contact.rb", "lib/diaspora_federation/entities/conversation.rb", "lib/diaspora_federation/entities/event.rb", "lib/diaspora_federation/entities/event_participation.rb", "lib/diaspora_federation/entities/like.rb", "lib/diaspora_federation/entities/location.rb", "lib/diaspora_federation/entities/message.rb", "lib/diaspora_federation/entities/participation.rb", "lib/diaspora_federation/entities/person.rb", "lib/diaspora_federation/entities/photo.rb", "lib/diaspora_federation/entities/poll.rb", "lib/diaspora_federation/entities/poll_answer.rb", "lib/diaspora_federation/entities/poll_participation.rb", "lib/diaspora_federation/entities/post.rb", "lib/diaspora_federation/entities/profile.rb", "lib/diaspora_federation/entities/related_entity.rb", "lib/diaspora_federation/entities/relayable.rb", "lib/diaspora_federation/entities/relayable_retraction.rb", "lib/diaspora_federation/entities/request.rb", "lib/diaspora_federation/entities/reshare.rb", "lib/diaspora_federation/entities/retraction.rb", "lib/diaspora_federation/entities/signable.rb", "lib/diaspora_federation/entities/signed_retraction.rb", "lib/diaspora_federation/entities/status_message.rb", "lib/diaspora_federation/entity.rb", "lib/diaspora_federation/federation.rb", "lib/diaspora_federation/federation/fetcher.rb", "lib/diaspora_federation/federation/receiver.rb", "lib/diaspora_federation/federation/receiver/abstract_receiver.rb", "lib/diaspora_federation/federation/receiver/exceptions.rb", "lib/diaspora_federation/federation/receiver/private.rb", "lib/diaspora_federation/federation/receiver/public.rb", "lib/diaspora_federation/federation/sender.rb", "lib/diaspora_federation/federation/sender/hydra_wrapper.rb", "lib/diaspora_federation/http_client.rb", "lib/diaspora_federation/logging.rb", "lib/diaspora_federation/parsers.rb", "lib/diaspora_federation/parsers/base_parser.rb", "lib/diaspora_federation/parsers/json_parser.rb", "lib/diaspora_federation/parsers/relayable_json_parser.rb", "lib/diaspora_federation/parsers/relayable_xml_parser.rb", "lib/diaspora_federation/parsers/xml_parser.rb", "lib/diaspora_federation/properties_dsl.rb", "lib/diaspora_federation/salmon.rb", "lib/diaspora_federation/salmon/aes.rb", "lib/diaspora_federation/salmon/encrypted_magic_envelope.rb", "lib/diaspora_federation/salmon/encrypted_slap.rb", "lib/diaspora_federation/salmon/exceptions.rb", "lib/diaspora_federation/salmon/magic_envelope.rb", "lib/diaspora_federation/salmon/slap.rb", "lib/diaspora_federation/salmon/xml_payload.rb", "lib/diaspora_federation/validators.rb", "lib/diaspora_federation/validators/account_deletion_validator.rb", "lib/diaspora_federation/validators/account_migration_validator.rb", "lib/diaspora_federation/validators/comment_validator.rb", "lib/diaspora_federation/validators/contact_validator.rb", "lib/diaspora_federation/validators/conversation_validator.rb", "lib/diaspora_federation/validators/event_participation_validator.rb", "lib/diaspora_federation/validators/event_validator.rb", "lib/diaspora_federation/validators/h_card_validator.rb", "lib/diaspora_federation/validators/like_validator.rb", "lib/diaspora_federation/validators/location_validator.rb", "lib/diaspora_federation/validators/message_validator.rb", "lib/diaspora_federation/validators/participation_validator.rb", "lib/diaspora_federation/validators/person_validator.rb", "lib/diaspora_federation/validators/photo_validator.rb", "lib/diaspora_federation/validators/poll_answer_validator.rb", "lib/diaspora_federation/validators/poll_participation_validator.rb", "lib/diaspora_federation/validators/poll_validator.rb", "lib/diaspora_federation/validators/profile_validator.rb", "lib/diaspora_federation/validators/related_entity_validator.rb", "lib/diaspora_federation/validators/relayable_validator.rb", "lib/diaspora_federation/validators/reshare_validator.rb", "lib/diaspora_federation/validators/retraction_validator.rb", "lib/diaspora_federation/validators/rules/birthday.rb", "lib/diaspora_federation/validators/rules/boolean.rb", "lib/diaspora_federation/validators/rules/diaspora_id.rb", "lib/diaspora_federation/validators/rules/diaspora_id_count.rb", "lib/diaspora_federation/validators/rules/guid.rb", "lib/diaspora_federation/validators/rules/not_nil.rb", "lib/diaspora_federation/validators/rules/public_key.rb", "lib/diaspora_federation/validators/rules/tag_count.rb", "lib/diaspora_federation/validators/status_message_validator.rb", "lib/diaspora_federation/validators/web_finger_validator.rb", "lib/diaspora_federation/version.rb"] s.homepage = "https://github.com/diaspora/diaspora_federation" s.licenses = ["AGPL-3.0"] s.require_paths = ["lib"] s.required_ruby_version = Gem::Requirement.new("~> 2.1") s.rubygems_version = "1.8.23" s.summary = "diaspora* federation library" if s.respond_to? :specification_version then s.specification_version = 4 if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then s.add_runtime_dependency(%q, ["< 0.13.0", ">= 0.9.0"]) s.add_runtime_dependency(%q, ["< 0.13.0", ">= 0.10.0"]) s.add_runtime_dependency(%q, [">= 1.6.8", "~> 1.6"]) s.add_runtime_dependency(%q, ["~> 1.0"]) s.add_runtime_dependency(%q, ["~> 1.0"]) else s.add_dependency(%q, ["< 0.13.0", ">= 0.9.0"]) s.add_dependency(%q, ["< 0.13.0", ">= 0.10.0"]) s.add_dependency(%q, [">= 1.6.8", "~> 1.6"]) s.add_dependency(%q, ["~> 1.0"]) s.add_dependency(%q, ["~> 1.0"]) end else s.add_dependency(%q, ["< 0.13.0", ">= 0.9.0"]) s.add_dependency(%q, ["< 0.13.0", ">= 0.10.0"]) s.add_dependency(%q, [">= 1.6.8", "~> 1.6"]) s.add_dependency(%q, ["~> 1.0"]) s.add_dependency(%q, ["~> 1.0"]) end end diaspora_federation-0.2.1/Changelog.md0000644000004100000410000002275213146064271017771 0ustar www-datawww-data# 0.2.1 ## Features Add `DiasporaFederation::Schemas` to access the JSON schema [#70](https://github.com/diaspora/diaspora_federation/pull/70) ## Refactor Don't add optional properties to generated XML and JSON when nil [#71](https://github.com/diaspora/diaspora_federation/pull/71) # 0.2.0 ## Features * Add JSON support to entities [#52](https://github.com/diaspora/diaspora_federation/pull/52) * Add `AccountMigration` entity [#54](https://github.com/diaspora/diaspora_federation/pull/54) * Add `public` flag to `Profile` entity [#59](https://github.com/diaspora/diaspora_federation/pull/59) * Allow to generate WebFinger with additional data [#61](https://github.com/diaspora/diaspora_federation/pull/61) [1b9dfc8](https://github.com/diaspora/diaspora_federation/commit/1b9dfc812e8b63c64a2d98db8999cae21d102c87) * Provide RFC 7033 WebFinger [#63](https://github.com/diaspora/diaspora_federation/pull/63) * Validate the author of the root post for a reshare [92ce4ea](https://github.com/diaspora/diaspora_federation/commit/92ce4eacf842f7a2fa74f298407062a4e0c891a3) ## Refactor * Replace `factory_girl` with `fabrication` [184954e](https://github.com/diaspora/diaspora_federation/commit/184954e09ce72242cb7ec06c15fed0ad7b6c57c6) * Use `actionpack` as dependency instead of `rails` (for `diaspora_federation-rails`) [f860a62](https://github.com/diaspora/diaspora_federation/commit/f860a62382999dcf0adaf41a24b50b74611f6ed9) * Remove old backward-compatibility from WebFinger [#60](https://github.com/diaspora/diaspora_federation/pull/60) * Make optional properties optional when generating WebFinger [#61](https://github.com/diaspora/diaspora_federation/pull/61) [5fef763](https://github.com/diaspora/diaspora_federation/commit/5fef7633c3aaf47db2592749e506f40b581c0371) * Make `Message` entity non-relayable (see [#36](https://github.com/diaspora/diaspora_federation/issues/36)) [#62](https://github.com/diaspora/diaspora_federation/pull/62) [b7167b9](https://github.com/diaspora/diaspora_federation/commit/b7167b9fde4d614fb8f7510720918e029d3624f4) * Make `Participation` entity non-relayable (see [#35](https://github.com/diaspora/diaspora_federation/issues/35)) [#62](https://github.com/diaspora/diaspora_federation/pull/62) [41ebe13](https://github.com/diaspora/diaspora_federation/commit/41ebe13126a28b95dbe5acc5db3939ee9dae7e4b) * Remove legacy signature order and order by property order in entity (see [#26](https://github.com/diaspora/diaspora_federation/issues/26)) [#62](https://github.com/diaspora/diaspora_federation/pull/62) [87033e4](https://github.com/diaspora/diaspora_federation/commit/87033e4cd63f7d237b9d02d95b739e971d205ea1) * Send new property names in XML (see [#29](https://github.com/diaspora/diaspora_federation/issues/29)) [#62](https://github.com/diaspora/diaspora_federation/pull/62) [52a8c89](https://github.com/diaspora/diaspora_federation/commit/52a8c89d4c0f1f66b188ab4a2ac36ffafb0bfa1a) * Send unwrapped entities (see [#28](https://github.com/diaspora/diaspora_federation/issues/28)) [#62](https://github.com/diaspora/diaspora_federation/pull/62) [221d87d](https://github.com/diaspora/diaspora_federation/commit/221d87d7fe664bde8718182178cb31ba532977c6) * Send the raw magic envelope and new encrypted magic envelope with crypt-json-wrapper (see [#30](https://github.com/diaspora/diaspora_federation/issues/30)) [#62](https://github.com/diaspora/diaspora_federation/pull/62) [1f99518](https://github.com/diaspora/diaspora_federation/commit/1f99518706e6bef3dca51453bf571373cd389942) [e5b2ef7](https://github.com/diaspora/diaspora_federation/commit/e5b2ef71e8cfa299874e3f80175526b8999839f7) * Remove sign-code and prevent creation of `SignedRetraction` and `RelayableRetraction` (see [#27](https://github.com/diaspora/diaspora_federation/issues/27)) [#62](https://github.com/diaspora/diaspora_federation/pull/62) [cd3a7ab](https://github.com/diaspora/diaspora_federation/commit/cd3a7abf4d778f7e3139bcb73a42a9dc4cbcb835) * Rename `xml_order` to `signature_order` on relayables [b510ed8](https://github.com/diaspora/diaspora_federation/commit/b510ed868f12e15fd5c7b91909cc35281efeb10e) * Prevent creation of `Request` entity (see [#32](https://github.com/diaspora/diaspora_federation/issues/32)) [#62](https://github.com/diaspora/diaspora_federation/pull/62) [deed1c3](https://github.com/diaspora/diaspora_federation/commit/deed1c3f3ea76658074a4e34f534a12f083e8622) * Don't check `parent_author_signature` and don't check the `author_signature` when the author is the parent author for relayables (see [#64](https://github.com/diaspora/diaspora_federation/issues/64)) [#65](https://github.com/diaspora/diaspora_federation/pull/65) [6817579](https://github.com/diaspora/diaspora_federation/commit/681757907204885735bc60b18929938ec2ad04bb) [57edc8b](https://github.com/diaspora/diaspora_federation/commit/57edc8baabcf884b0ac5395266ffe148cff5da1d) * Add `created_at` to `Comment` entity [#67](https://github.com/diaspora/diaspora_federation/pull/67) * Improve logging when validation fails [c0ea38d](https://github.com/diaspora/diaspora_federation/commit/c0ea38d258ccd76a7499bff0197434d8e42768e8) ## Bug fixes * Fix issues when used without rails [ed2c2b7](https://github.com/diaspora/diaspora_federation/commit/ed2c2b7f47b91c308321076344459aee839318a8) [b25e229](https://github.com/diaspora/diaspora_federation/commit/b25e2293b0b83bc083bccdbf1523ee691dbb7b2e) [6615233](https://github.com/diaspora/diaspora_federation/commit/66152337f2b47c1cf6639646f55d21f69fe99708) # 0.1.9 ## Bug fixes * Don't log encrypted private messages [8859c96](https://github.com/diaspora/diaspora_federation/commit/8859c960ac2b771399ad42ccf795043aea4ec9a5) # 0.1.8 ## Feature * Add ruby 2.4 support ## Documentation * Various improvements in the protocol documentation # 0.1.7 ## Feature * Add event entities [#44](https://github.com/diaspora/diaspora_federation/pull/44) ## Refactor * Add generated signatures of relayables to `#to_h` [#48](https://github.com/diaspora/diaspora_federation/pull/48) ## Bug fixes * Fix parsing of false value [9a7fd27](https://github.com/diaspora/diaspora_federation/commit/9a7fd278b528c809b3a8c53b86c5fa8d6efaf8aa) # 0.1.6 ## Feature * Add rails 5 support [82ea57e](https://github.com/diaspora/diaspora_federation/commit/82ea57ef34fe25d2ffbd6067171d73802735043b) ## Refactor * Add property types [#43](https://github.com/diaspora/diaspora_federation/pull/43) * Change timestamp format to ISO 8601 [#43](https://github.com/diaspora/diaspora_federation/pull/43) * Move protocol documentation to master branch [a15d285](https://github.com/diaspora/diaspora_federation/commit/a15d285a6e778a04c5e0c2f9428be099d6abddce) # 0.1.5 ## Refactor * Use `head` method instead of `:nothing` option [44f6527](https://github.com/diaspora/diaspora_federation/commit/44f6527d64489c212c0f6b050ad343ea0e53e964) * Add `sender` parameter to `:receive_entity` callback [fb60f83](https://github.com/diaspora/diaspora_federation/commit/fb60f8392698f49b9291f3461e7a68ec84def9e2) ## Bug fixes * HydraWrapper: Validate hostname after redirect [d18e623](https://github.com/diaspora/diaspora_federation/commit/d18e623082ac620a89e0542ceb97a9f2501c16bf) # 0.1.4 ## Refactor * Improve magic envelope validation [90d12e7](https://github.com/diaspora/diaspora_federation/commit/90d12e71d00bd4874c09f81cde968360111933f9) * Raise ValidationError if properties are missing [4295237](https://github.com/diaspora/diaspora_federation/commit/4295237e9e6e8e0ff23a5d8d732654b865f44944) # 0.1.3 ## Refactor * Improve handling of `xml_order` in relayables [36a787d](https://github.com/diaspora/diaspora_federation/commit/36a787dd87f9770e16fbc1bbc0a6c0d6f059e727) [ba129aa](https://github.com/diaspora/diaspora_federation/commit/ba129aafa38f978f69565d39b7a881a245b03bab) [41de99b](https://github.com/diaspora/diaspora_federation/commit/41de99bd5e4ed2779d574183a58f9ac9550c658a) # 0.1.2 ## Refactor * Improve code documentation [#38](https://github.com/diaspora/diaspora_federation/pull/38) * Improve validation [9b32315](https://github.com/diaspora/diaspora_federation/commit/9b3231583d85e6007bf43cedc4480f043c8bde15) [eb8cdef](https://github.com/diaspora/diaspora_federation/commit/eb8cdef604cc8fe71e8455f36a317d80657f1582) [0980294](https://github.com/diaspora/diaspora_federation/commit/0980294a0d259cba1fa2a2a655163b3fa844d239) * Photo: `status_message_guid` is optional [4136fb9](https://github.com/diaspora/diaspora_federation/commit/4136fb973e7ad27158ef605df12727f4e959c3a3) * A GUID is at most 255 chars long [f7d269c](https://github.com/diaspora/diaspora_federation/commit/f7d269cd6a4c1b48a7b34083f5fea04ac0835a48) * hCard: `nickname` is optional [4b94949](https://github.com/diaspora/diaspora_federation/commit/4b949491df3a16b30f6e27113d6fa95c165c1edc) * StatusMessage: Rename `raw_message` to `text` [2aaff56](https://github.com/diaspora/diaspora_federation/commit/2aaff56e147b505626a615d60564fbbf22c2f452) [#29](https://github.com/diaspora/diaspora_federation/issues/29) ## Bug fixes * Do not reuse cURL sockets to avoid issues caused by too many simultaneous connections [#37](https://github.com/diaspora/diaspora_federation/pull/37) * Handle empty xml-elements for nested entities [26b7991](https://github.com/diaspora/diaspora_federation/commit/26b7991defe1d84d10c1186a151676076946b26f) * Gracefully handle missing xml elements of relayables [9097097](https://github.com/diaspora/diaspora_federation/commit/90970973a58cbc3d897d21a43c0a6c93a30605be) # 0.1.1 ## Features * Fetch root posts for reshares [9b090a3](https://github.com/diaspora/diaspora_federation/commit/9b090a39501705f00403f124e215e78866039f1e) # 0.1.0 ## Features * Added Salmon support diaspora_federation-0.2.1/lib/0000755000004100000410000000000013146064271016316 5ustar www-datawww-datadiaspora_federation-0.2.1/lib/diaspora_federation/0000755000004100000410000000000013146064271022320 5ustar www-datawww-datadiaspora_federation-0.2.1/lib/diaspora_federation/entity.rb0000644000004100000410000003005613146064271024165 0ustar www-datawww-datamodule DiasporaFederation # +Entity+ is the base class for all other objects used to encapsulate data # for federation messages in the diaspora* network. # Entity fields are specified using a simple {PropertiesDSL DSL} as part of # the class definition. # # Any entity also provides the means to serialize itself and all nested # entities to XML (for deserialization from XML to +Entity+ instances, see # {Salmon::XmlPayload}). # # @abstract Subclass and specify properties to implement various entities. # # @example Entity subclass definition # class MyEntity < Entity # property :prop # property :optional, default: false # property :dynamic_default, default: -> { Time.now } # property :another_prop, xml_name: :another_name # entity :nested, NestedEntity # entity :multiple, [OtherEntity] # end # # @example Entity instantiation # nentity = NestedEntity.new # oe1 = OtherEntity.new # oe2 = OtherEntity.new # # entity = MyEntity.new(prop: 'some property', # nested: nentity, # multiple: [oe1, oe2]) # # @note Entity properties can only be set during initialization, after that the # entity instance becomes frozen and must not be modified anymore. Instances # are intended to be immutable data containers, only. class Entity extend PropertiesDSL include Logging # Invalid XML characters # @see https://www.w3.org/TR/REC-xml/#charsets "Extensible Markup Language (XML) 1.0" INVALID_XML_REGEX = /[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]/ # Initializes the Entity with the given attribute hash and freezes the created # instance it returns. # # After creation, the entity is validated against a Validator, if one is defined. # The Validator needs to be in the {DiasporaFederation::Validators} namespace and # named like "Validator". Only valid entities can be created. # # @see DiasporaFederation::Validators # # @note Attributes not defined as part of the class definition ({PropertiesDSL#property}, # {PropertiesDSL#entity}) get discarded silently. # # @param [Hash] data entity data # @return [Entity] new instance def initialize(data) logger.debug "create entity #{self.class} with data: #{data}" raise ArgumentError, "expected a Hash" unless data.is_a?(Hash) entity_data = self.class.resolv_aliases(data) validate_missing_props(entity_data) self.class.default_values.merge(entity_data).each do |name, value| instance_variable_set("@#{name}", instantiate_nested(name, nilify(value))) if setable?(name, value) end freeze validate end # Returns a Hash representing this Entity (attributes => values). # Nested entities are also converted to a Hash. # @return [Hash] entity data (mostly equal to the hash used for initialization). def to_h enriched_properties.map {|key, value| type = self.class.class_props[key] if type.instance_of?(Symbol) || value.nil? [key, value] elsif type.instance_of?(Class) [key, value.to_h] elsif type.instance_of?(Array) [key, value.map(&:to_h)] end }.to_h end # Returns the XML representation for this entity constructed out of # {http://www.rubydoc.info/gems/nokogiri/Nokogiri/XML/Element Nokogiri::XML::Element}s # # @see Nokogiri::XML::Node.to_xml # # @return [Nokogiri::XML::Element] root element containing properties as child elements def to_xml doc = Nokogiri::XML::DocumentFragment.new(Nokogiri::XML::Document.new) Nokogiri::XML::Element.new(self.class.entity_name, doc).tap do |root_element| xml_elements.each do |name, value| add_property_to_xml(doc, root_element, name, value) end end end # Construct a new instance of the given Entity and populate the properties # with the attributes found in the XML. # Works recursively on nested Entities and Arrays thereof. # # @param [Nokogiri::XML::Element] root_node xml nodes # @return [Entity] instance def self.from_xml(root_node) from_hash(*xml_parser_class.new(self).parse(root_node)) end private_class_method def self.xml_parser_class DiasporaFederation::Parsers::XmlParser end # Creates an instance of self by parsing a hash in the format of JSON serialized object (which usually means # data from a parsed JSON input). def self.from_json(json_hash) from_hash(*json_parser_class.new(self).parse(json_hash)) end private_class_method def self.json_parser_class DiasporaFederation::Parsers::JsonParser end # Makes an underscored, lowercase form of the class name # # @see .entity_class # # @return [String] entity name def self.entity_name class_name.tap do |word| word.gsub!(/(.)([A-Z])/, '\1_\2') word.downcase! end end # Transform the given String from the lowercase underscored version to a # camelized variant and returns the Class constant. # # @see .entity_name # # @param [String] entity_name "snake_case" class name # @return [Class] entity class def self.entity_class(entity_name) raise InvalidEntityName, "'#{entity_name}' is invalid" unless entity_name =~ /\A[a-z]*(_[a-z]*)*\z/ class_name = entity_name.sub(/\A[a-z]/, &:upcase) class_name.gsub!(/_([a-z])/) { Regexp.last_match[1].upcase } raise UnknownEntity, "'#{class_name}' not found" unless Entities.const_defined?(class_name) Entities.const_get(class_name) end # @return [String] class name as string def self.class_name name.rpartition("::").last end # @return [String] string representation of this object def to_s "#{self.class.class_name}#{":#{guid}" if respond_to?(:guid)}" end # Renders entity to a hash representation of the entity JSON format # @return [Hash] Returns a hash that is equal by structure to the entity in JSON format def to_json { entity_type: self.class.entity_name, entity_data: json_data } end # Creates an instance of self, filling it with data from a provided hash of properties. # # The hash format is described as following:
# 1) Properties of the hash are representation of the entity's class properties
# 2) Keys of the hash must be of Symbol type
# 3) Possible values of the hash properties depend on the types of the entity's class properties
# 4) Basic properties, such as booleans, strings, integers and timestamps are represented by values of respective # formats
# 5) Nested hashes and arrays of hashes are allowed to represent nested entities. Nested hashes follow the same # format as the parent hash.
# 6) Besides, the nested entities can be passed in the hash as already instantiated objects of the respective type. # # @param [Hash] properties_hash A hash of the expected format # @return [Entity] an instance def self.from_hash(properties_hash) new(properties_hash) end private def validate_missing_props(entity_data) missing_props = self.class.missing_props(entity_data) return if missing_props.empty? obj_str = "#{self.class.class_name}#{":#{entity_data[:guid]}" if entity_data.has_key?(:guid)}" \ "#{" from #{entity_data[:author]}" if entity_data.has_key?(:author)}" raise ValidationError, "#{obj_str}: Missing required properties: #{missing_props.join(', ')}" end def setable?(name, val) type = self.class.class_props[name] return false if type.nil? # property undefined setable_property?(type, val) || setable_nested?(type, val) || setable_multi?(type, val) end def setable_property?(type, val) setable_string?(type, val) || type == :timestamp && val.is_a?(Time) end def setable_string?(type, val) %i[string integer boolean].include?(type) && val.respond_to?(:to_s) end def setable_nested?(type, val) type.instance_of?(Class) && type.ancestors.include?(Entity) && (val.is_a?(Entity) || val.is_a?(Hash)) end def setable_multi?(type, val) type.instance_of?(Array) && val.instance_of?(Array) && (val.all? {|v| v.instance_of?(type.first) } || val.all? {|v| v.instance_of?(Hash) }) end def nilify(value) return nil if value.respond_to?(:empty?) && value.empty? && !value.instance_of?(Array) value end def instantiate_nested(name, value) if value.instance_of?(Array) return value unless value.first.instance_of?(Hash) value.map {|hash| self.class.class_props[name].first.new(hash) } elsif value.instance_of?(Hash) self.class.class_props[name].new(value) else value end end def validate validator_name = "#{self.class.name.split('::').last}Validator" return unless Validators.const_defined? validator_name validator_class = Validators.const_get validator_name validator = validator_class.new self raise ValidationError, error_message(validator) unless validator.valid? end def error_message(validator) errors = validator.errors.map do |prop, rule| "property: #{prop}, value: #{public_send(prop).inspect}, rule: #{rule[:rule]}, with params: #{rule[:params]}" end "Failed validation for #{self}#{" from #{author}" if respond_to?(:author)} for properties: #{errors.join(' | ')}" end # @return [Hash] hash with all properties def properties self.class.class_props.keys.each_with_object({}) do |prop, hash| hash[prop] = public_send(prop) end end def normalized_properties properties.map {|name, value| [name, normalize_property(name, value)] }.to_h end def normalize_property(name, value) return nil if optional_nil_value?(name, value) case self.class.class_props[name] when :string value.to_s when :timestamp value.nil? ? "" : value.utc.iso8601 else value end end # default: nothing to enrich def enriched_properties normalized_properties end # default: no special order def xml_elements enriched_properties end def add_property_to_xml(doc, root_element, name, value) if [String, TrueClass, FalseClass, Integer].any? {|c| value.is_a? c } root_element << simple_node(doc, name, value.to_s) else # call #to_xml for each item and append to root [*value].compact.each do |item| child = item.to_xml root_element << child if child end end end # Create simple node, fill it with text and append to root def simple_node(doc, name, value) Nokogiri::XML::Element.new(name.to_s, doc).tap do |node| node.content = value.gsub(INVALID_XML_REGEX, "\uFFFD") unless value.empty? end end # Generates a hash with entity properties which is put to the "entity_data" # field of a JSON serialized object. # @return [Hash] object properties in JSON format def json_data enriched_properties.map {|key, value| type = self.class.class_props[key] next if optional_nil_value?(key, value) if !value.nil? && type.instance_of?(Class) entity_data = value.to_json [key, entity_data] unless entity_data.nil? elsif type.instance_of?(Array) entity_data = value.nil? ? nil : value.map(&:to_json) [key, entity_data] unless entity_data.nil? else [key, value] end }.compact.to_h end def optional_nil_value?(name, value) value.nil? && self.class.optional_props.include?(name) end # Raised, if entity is not valid class ValidationError < RuntimeError end # Raised, if the entity name in the XML is invalid class InvalidEntityName < RuntimeError end # Raised, if the entity contained within the XML cannot be mapped to a # defined {Entity} subclass. class UnknownEntity < RuntimeError end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/0000755000004100000410000000000013146064271024144 5ustar www-datawww-datadiaspora_federation-0.2.1/lib/diaspora_federation/entities/poll_answer.rb0000644000004100000410000000102613146064271027015 0ustar www-datawww-datamodule DiasporaFederation module Entities # This entity represents a poll answer and is federated as a part of the Poll entity. # # @see Validators::PollAnswerValidator class PollAnswer < Entity # @!attribute [r] guid # A random string of at least 16 chars # @see Validation::Rule::Guid # @return [String] guid property :guid, :string # @!attribute [r] answer # Text of the answer # @return [String] answer property :answer, :string end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/conversation.rb0000644000004100000410000000301113146064271027176 0ustar www-datawww-datamodule DiasporaFederation module Entities # This entity represents a private conversation between users. # # @see Validators::ConversationValidator class Conversation < Entity # @!attribute [r] author # The diaspora* ID of the person initiated the conversation # @see Person#author # @return [String] diaspora* ID property :author, :string, xml_name: :diaspora_handle # @!attribute [r] guid # A random string of at least 16 chars # @see Validation::Rule::Guid # @return [String] conversation guid property :guid, :string # @!attribute [r] subject # @return [String] the conversation subject property :subject, :string # @!attribute [r] created_at # @return [Time] Conversation creation time property :created_at, :timestamp, default: -> { Time.now.utc } # @!attribute [r] participants # The diaspora* IDs of the persons participating the conversation separated by ";" # @return [String] participants diaspora* IDs property :participants, :string, xml_name: :participant_handles # @!attribute [r] messages # @return [[Entities::Message]] Messages of this conversation entity :messages, [Entities::Message], default: [] private def validate super messages.each do |message| raise ValidationError, "nested #{message} has different author: author=#{author}" if message.author != author end end end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/status_message.rb0000644000004100000410000000316513146064271027525 0ustar www-datawww-datamodule DiasporaFederation module Entities # This entity represents a status message sent by a user. # # @see Validators::StatusMessageValidator class StatusMessage < Entity include Post # @!attribute [r] text # Text of the status message composed by the user # @return [String] text of the status message property :text, :string, xml_name: :raw_message # @!attribute [r] photos # Optional photos attached to the status message # @return [[Entities::Photo]] photos entity :photos, [Entities::Photo], optional: true, default: [] # @!attribute [r] location # Optional location attached to the status message # @return [Entities::Location] location entity :location, Entities::Location, optional: true # @!attribute [r] poll # Optional poll attached to the status message # @return [Entities::Poll] poll entity :poll, Entities::Poll, optional: true # @!attribute [r] event # Optional event attached to the status message # @return [Entities::Event] event entity :event, Entities::Event, optional: true # @!attribute [r] public # Shows whether the status message is visible to everyone or only to some aspects # @return [Boolean] is it public property :public, :boolean, default: false private def validate super photos.each do |photo| if photo.author != author raise ValidationError, "nested #{photo} has different author: author=#{author} obj=#{self}" end end end end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/location.rb0000644000004100000410000000127113146064271026302 0ustar www-datawww-datamodule DiasporaFederation module Entities # This entity is used to specify location data and used embedded in a status message. # # @see Validators::LocationValidator class Location < Entity # @!attribute [r] address # A string describing your location, e.g. a city name, a street name, etc # @return [String] address property :address, :string # @!attribute [r] lat # Geographical latitude of your location # @return [String] latitude property :lat, :string # @!attribute [r] lng # Geographical longitude of your location # @return [String] longitude property :lng, :string end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/profile.rb0000644000004100000410000000521613146064271026135 0ustar www-datawww-datamodule DiasporaFederation module Entities # This entity contains all the profile data of a person. # # @see Validators::ProfileValidator class Profile < Entity # @!attribute [r] author # The diaspora* ID of the person # @see Person#author # @return [String] diaspora* ID # @!attribute [r] diaspora_id # Alias for author # @see Profile#author # @return [String] diaspora* ID property :author, :string, alias: :diaspora_id, xml_name: :diaspora_handle # @!attribute [r] first_name # @deprecated We decided to only use one name field, these should be removed # in later iterations (will affect older diaspora* installations). # @see #full_name # @see Discovery::HCard#first_name # @return [String] first name property :first_name, :string, optional: true # @!attribute [r] last_name # @deprecated We decided to only use one name field, these should be removed # in later iterations (will affect older diaspora* installations). # @see #full_name # @see Discovery::HCard#last_name # @return [String] last name property :last_name, :string, optional: true # @!attribute [r] image_url # @see Discovery::HCard#photo_large_url # @return [String] url to the big avatar (300x300) property :image_url, :string, optional: true # @!attribute [r] image_url_medium # @see Discovery::HCard#photo_medium_url # @return [String] url to the medium avatar (100x100) property :image_url_medium, :string, optional: true # @!attribute [r] image_url_small # @see Discovery::HCard#photo_small_url # @return [String] url to the small avatar (50x50) property :image_url_small, :string, optional: true property :birthday, :string, optional: true property :gender, :string, optional: true property :bio, :string, optional: true property :location, :string, optional: true # @!attribute [r] searchable # @see Discovery::HCard#searchable # @return [Boolean] searchable flag property :searchable, :boolean, optional: true, default: true # @!attribute [r] public # Shows whether the profile is visible to everyone or only to contacts # @return [Boolean] is it public property :public, :boolean, optional: true, default: false property :nsfw, :boolean, optional: true, default: false property :tag_string, :string, optional: true # @return [String] string representation of this object def to_s "Profile:#{author}" end end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/event_participation.rb0000644000004100000410000000112213146064271030534 0ustar www-datawww-datamodule DiasporaFederation module Entities # This entity represents a participation in an event, i.e. it is issued when a user responds to en event. # # @see Validators::EventParticipationValidator class EventParticipation < Entity # The {EventParticipation} parent is an {Event} PARENT_TYPE = "Event".freeze include Relayable # @!attribute [r] status # The participation status of the user # "accepted", "declined" or "tentative" # @return [String] event participation status property :status, :string end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/related_entity.rb0000644000004100000410000000326313146064271027511 0ustar www-datawww-datamodule DiasporaFederation module Entities # Entity meta informations for a related entity (parent or target of # another entity). class RelatedEntity < Entity # @!attribute [r] author # The diaspora* ID of the author # @see Person#author # @return [String] diaspora* ID property :author, :string # @!attribute [r] local # +true+ if the owner of the entity is local on the pod # @return [Boolean] is it a like or a dislike property :local, :boolean # @!attribute [r] public # Shows whether the entity is visible to everyone or only to some aspects # @return [Boolean] is it public property :public, :boolean, default: false # @!attribute [r] parent # Parent if the entity also has a parent (Comment or Like) or +nil+ if it has no parent # @return [RelatedEntity] parent entity entity :parent, Entities::RelatedEntity, default: nil # Get related entity from the backend or fetch it from remote if not available locally # @return [RelatedEntity] fetched related entity def self.fetch(author, type, guid) # Try to fetch locally entity = DiasporaFederation.callbacks.trigger(:fetch_related_entity, type, guid) return entity if entity # Fetch and receive entity from remote if not available locally Federation::Fetcher.fetch_public(author, type, guid) DiasporaFederation.callbacks.trigger(:fetch_related_entity, type, guid) end # never add {RelatedEntity} to xml def to_xml nil end # never add {RelatedEntity} to json def to_json nil end end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/comment.rb0000644000004100000410000000113513146064271026133 0ustar www-datawww-datamodule DiasporaFederation module Entities # This entity represents a comment to some kind of post (e.g. status message). # # @see Validators::CommentValidator class Comment < Entity # The {Comment} parent is a {Post} PARENT_TYPE = "Post".freeze include Relayable # @!attribute [r] text # @return [String] the comment text property :text, :string # @!attribute [r] created_at # Comment entity creation time # @return [Time] creation time property :created_at, :timestamp, default: -> { Time.now.utc } end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/relayable_retraction.rb0000644000004100000410000000572613146064271030675 0ustar www-datawww-datamodule DiasporaFederation module Entities # This entity represents a claim of deletion of a previously federated # relayable entity. ({Entities::Comment}, {Entities::Like}) # # There are two cases of federation of the RelayableRetraction. # Retraction from the dowstream object owner is when an author of the # relayable (e.g. Comment) deletes it themself. In this case only target_author_signature # is filled and a retraction is sent to the commented post's author. Here # the upstream object owner signs it with the parent's author key, puts # the signature in parent_author_signature and sends it to other pods where # other participating people are present. This is the second case - retraction # from the upstream object owner. # Retraction from the upstream object owner can also be performed by the # upstream object owner themself - they have a right to delete comments on their posts. # In any case in the retraction by the upstream author target_author_signature # is not checked, only parent_author_signature is checked. # # @see Validators::RelayableRetractionValidator # @deprecated will be replaced with {Entities::Retraction} class RelayableRetraction < Entity # @!attribute [r] parent_author_signature # Contains a signature of the entity using the private key of the author of a parent post. # This signature is mandatory only when federating from an upstream author to the subscribers. # @see Relayable#parent_author_signature # @return [String] parent author signature property :parent_author_signature, :string, default: nil # @!attribute [r] target_guid # Guid of a relayable to be deleted # @see Comment#guid # @return [String] target guid property :target_guid, :string # @!attribute [r] target_type # A string describing a type of the target # @see Retraction#target_type # @return [String] target type property :target_type, :string # @!attribute [r] author # The diaspora* ID of the person who deletes a relayable # @see Person#author # @return [String] diaspora* ID property :author, :string, xml_name: :sender_handle # @!attribute [r] target_author_signature # Contains a signature of the entity using the private key of the # author of a federated relayable entity. ({Entities::Comment}, {Entities::Like}) # This signature is mandatory only when federation from the subscriber to an upstream # author is done. # @see Relayable#author_signature # @return [String] target author signature property :target_author_signature, :string, default: nil def initialize(*) raise "Sending RelayableRetraction is not supported anymore! Use Retraction instead!" end # @return [Retraction] instance def self.from_hash(hash) Retraction.from_hash(hash) end end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/post.rb0000644000004100000410000000247213146064271025463 0ustar www-datawww-datamodule DiasporaFederation module Entities # This is a module that defines common properties for a post which # include {StatusMessage} and {Reshare}. module Post # On inclusion of this module the required properties for a post are added to the object that includes it. # # @!attribute [r] author # The diaspora* ID of the person who posts the post # @see Person#author # @return [String] diaspora* ID # # @!attribute [r] guid # A random string of at least 16 chars # @see Validation::Rule::Guid # @return [String] status message guid # # @!attribute [r] created_at # Post entity creation time # @return [Time] creation time # # @!attribute [r] provider_display_name # A string that describes a means by which a user has posted the post # @return [String] provider display name # # @param [Entity] entity the entity in which it is included def self.included(entity) entity.class_eval do property :author, :string, xml_name: :diaspora_handle property :guid, :string property :created_at, :timestamp, default: -> { Time.now.utc } property :provider_display_name, :string, optional: true end end end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/reshare.rb0000644000004100000410000000307013146064271026122 0ustar www-datawww-datamodule DiasporaFederation module Entities # This entity represents the fact that a user reshared another user's post. # # @see Validators::ReshareValidator class Reshare < Entity include Post # @!attribute [r] root_author # The diaspora* ID of the person who posted the original post # @see Person#author # @return [String] diaspora* ID property :root_author, :string, xml_name: :root_diaspora_id # @!attribute [r] root_guid # Guid of the original post # @see StatusMessage#guid # @return [String] root guid property :root_guid, :string # @!attribute [r] public # Has no meaning at the moment # @return [Boolean] public property :public, :boolean, optional: true, default: true # always true? (we only reshare public posts) # @return [String] string representation of this object def to_s "#{super}:#{root_guid}" end # Fetch and receive root post from remote, if not available locally # and validates if it's from the correct author def validate_root root = RelatedEntity.fetch(root_author, "Post", root_guid) return if root_author == root.author raise Entity::ValidationError, "root_author mismatch: obj=#{self} root_author=#{root_author} known_root_author=#{root.author}" end # Fetch root post after parse # @see Entity.from_hash # @return [Entity] instance def self.from_hash(hash) super.tap(&:validate_root) end end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/like.rb0000644000004100000410000000143513146064271025420 0ustar www-datawww-datamodule DiasporaFederation module Entities # This entity represents a like to some kind of post (e.g. status message). # # @see Validators::LikeValidator class Like < Entity include Relayable # @!attribute [r] parent_type # A string describing the type of the parent # Can be "Post" or "Comment" (Comments are currently not implemented in the # diaspora* frontend). # @return [String] parent type property :parent_type, :string, xml_name: :target_type # @!attribute [r] positive # If +true+ set a like, if +false+, set a dislike (dislikes are currently not # implemented in the diaspora* frontend). # @return [Boolean] is it a like or a dislike property :positive, :boolean end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/signable.rb0000644000004100000410000000415513146064271026262 0ustar www-datawww-datamodule DiasporaFederation module Entities # Signable is a module that encapsulates basic signature generation/verification flow for entities. module Signable include Logging # Digest instance used for signing DIGEST = OpenSSL::Digest::SHA256.new # Sign the data with the key # # @param [OpenSSL::PKey::RSA] privkey An RSA key # @return [String] A Base64 encoded signature of #signature_data with key def sign_with_key(privkey) Base64.strict_encode64(privkey.sign(DIGEST, signature_data)) end # Check that signature is a correct signature # # @param [String] author The author of the signature # @param [String] signature_key The signature to be verified # @raise [SignatureVerificationFailed] if the signature is not valid # @raise [PublicKeyNotFound] if no public key is found def verify_signature(author, signature_key) pubkey = DiasporaFederation.callbacks.trigger(:fetch_public_key, author) raise PublicKeyNotFound, "signature=#{signature_key} person=#{author} obj=#{self}" if pubkey.nil? signature = public_send(signature_key) raise SignatureVerificationFailed, "no #{signature_key} for #{self}" if signature.nil? valid = pubkey.verify(DIGEST, Base64.decode64(signature), signature_data) raise SignatureVerificationFailed, "wrong #{signature_key} for #{self}" unless valid logger.info "event=verify_signature signature=#{signature_key} status=valid obj=#{self}" end # This method defines what data is used for a signature creation/verification # # @abstract # @return [String] a string to sign def signature_data raise NotImplementedError.new("you must override this method to define signature base string") end # Raised, if verify_signatures fails to verify signatures (no public key found) class PublicKeyNotFound < RuntimeError end # Raised, if verify_signatures fails to verify signatures (signatures are wrong) class SignatureVerificationFailed < RuntimeError end end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/relayable.rb0000644000004100000410000002152213146064271026433 0ustar www-datawww-datamodule DiasporaFederation module Entities # This is a module that defines common properties for relayable entities # which include Like, Comment, Participation, Message, etc. Each relayable # has a parent, identified by guid. Relayables are also signed and signing/verification # logic is embedded into Salmon XML processing code. module Relayable include Signable # Additional properties from parsed input object # @return [Hash] additional elements attr_reader :additional_data # On inclusion of this module the required properties for a relayable are added to the object that includes it. # # @!attribute [r] author # The diaspora* ID of the author # @see Person#author # @return [String] diaspora* ID # # @!attribute [r] guid # A random string of at least 16 chars # @see Validation::Rule::Guid # @return [String] comment guid # # @!attribute [r] parent_guid # @see StatusMessage#guid # @return [String] parent guid # # @!attribute [r] author_signature # Contains a signature of the entity using the private key of the author of a relayable itself. # The presence of this signature is mandatory. Without it the entity won't be accepted by # a target pod. # @return [String] author signature # # @!attribute [r] parent_author_signature # Contains a signature of the entity using the private key of the author of a parent post. # @deprecated This signature isn't required anymore, because we can check the signature from # the parent author in the MagicEnvelope. # @return [String] parent author signature # # @!attribute [r] parent # Meta information about the parent object # @return [RelatedEntity] parent entity # # @param [Entity] klass the entity in which it is included def self.included(klass) klass.class_eval do property :author, :string, xml_name: :diaspora_handle property :guid, :string property :parent_guid, :string property :author_signature, :string, default: nil property :parent_author_signature, :string, default: nil entity :parent, Entities::RelatedEntity end klass.extend Parsing end # Initializes a new relayable Entity with order and additional xml elements # # @param [Hash] data entity data # @param [Array] signature_order order for the signature # @param [Hash] additional_data additional xml elements # @see DiasporaFederation::Entity#initialize def initialize(data, signature_order=nil, additional_data={}) self.signature_order = signature_order if signature_order @additional_data = additional_data super(data) end # Verifies the +author_signature+ if needed. # @see DiasporaFederation::Entities::Signable#verify_signature # # @raise [SignatureVerificationFailed] if the signature is not valid # @raise [PublicKeyNotFound] if no public key is found def verify_signature super(author, :author_signature) unless author == parent.author end def sender_valid?(sender) (sender == author && parent.local) || sender == parent.author end # @return [String] string representation of this object def to_s "#{super}#{":#{parent_type}" if respond_to?(:parent_type)}:#{parent_guid}" end def to_json super.merge!(property_order: signature_order).tap {|json_hash| missing_properties = json_hash[:property_order] - json_hash[:entity_data].keys missing_properties.each {|property| json_hash[:entity_data][property] = nil } } end # The order for signing # @return [Array] def signature_order @signature_order || self.class.class_props.keys.reject {|key| self.class.optional_props.include?(key) && public_send(key).nil? } - %i[author_signature parent_author_signature parent] end private # Sign with author key # @raise [AuthorPrivateKeyNotFound] if the author private key is not found # @return [String] A Base64 encoded signature of #signature_data with key def sign_with_author privkey = DiasporaFederation.callbacks.trigger(:fetch_private_key, author) raise AuthorPrivateKeyNotFound, "author=#{author} obj=#{self}" if privkey.nil? sign_with_key(privkey).tap do logger.info "event=sign status=complete signature=author_signature author=#{author} obj=#{self}" end end # Sign with parent author key, if the parent author is local (if the private key is found) # @return [String] A Base64 encoded signature of #signature_data with key def sign_with_parent_author_if_available privkey = DiasporaFederation.callbacks.trigger(:fetch_private_key, parent.author) return unless privkey sign_with_key(privkey).tap do logger.info "event=sign status=complete signature=parent_author_signature obj=#{self}" end end # Update the signatures with the keys of the author and the parent # if the signatures are not there yet and if the keys are available. # # @return [Hash] properties with updated signatures def enriched_properties super.merge(additional_data).tap do |hash| hash[:author_signature] = author_signature || sign_with_author hash.delete(:parent_author_signature) end end # Sort all XML elements according to the order used for the signatures. # # @return [Hash] sorted xml elements def xml_elements data = super.tap do |hash| hash[:parent_author_signature] = parent_author_signature || sign_with_parent_author_if_available.to_s end order = signature_order + %i[author_signature parent_author_signature] order.map {|element| [element, data[element] || ""] }.to_h end def signature_order=(order) prop_names = self.class.class_props.keys.map(&:to_s) @signature_order = order.reject {|name| name =~ /signature/ } .map {|name| prop_names.include?(name) ? name.to_sym : name } end # @return [String] signature data string def signature_data data = normalized_properties.merge(additional_data) signature_order.map {|name| data[name] }.join(";") end # Override class methods from {Entity} to parse serialized data module Parsing # Does the same job as Entity.from_hash except of the following differences: # 1) unknown properties from the properties_hash are stored to additional_data of the relayable instance # 2) parent entity fetch is attempted # 3) signatures verification is performed; property_order is used as the order in which properties are composed # to compute signatures # 4) unknown properties' keys must be of String type # # @see Entity.from_hash def from_hash(properties_hash, property_order) # Use all known properties to build the Entity (entity_data). All additional elements # are respected and attached to a hash as string (additional_data). This is needed # to support receiving objects from the future versions of diaspora*, where new elements may have been added. additional_data = properties_hash.reject {|key, _| class_props.has_key?(key) } fetch_parent(properties_hash) new(properties_hash, property_order, additional_data).tap(&:verify_signature) end private def fetch_parent(data) type = data.fetch(:parent_type) { break self::PARENT_TYPE if const_defined?(:PARENT_TYPE) raise DiasporaFederation::Entity::ValidationError, error_message_missing_property(data, "parent_type") } guid = data.fetch(:parent_guid) { raise DiasporaFederation::Entity::ValidationError, error_message_missing_property(data, "parent_guid") } data[:parent] = RelatedEntity.fetch(data[:author], type, guid) end def error_message_missing_property(data, missing_property) obj_str = "#{class_name}#{":#{data[:guid]}" if data.has_key?(:guid)}" \ "#{" from #{data[:author]}" if data.has_key?(:author)}" "Invalid #{obj_str}! Missing '#{missing_property}'." end def xml_parser_class DiasporaFederation::Parsers::RelayableXmlParser end def json_parser_class DiasporaFederation::Parsers::RelayableJsonParser end end # Raised, if creating the author_signature fails, because the private key was not found class AuthorPrivateKeyNotFound < RuntimeError end end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/request.rb0000644000004100000410000000174613146064271026171 0ustar www-datawww-datamodule DiasporaFederation module Entities # This entity represents a sharing request for a user. A user issues it # when they start sharing with another user. # # @see Validators::RequestValidator # @deprecated will be replaced with {Contact} class Request < Entity # @!attribute [r] author # The diaspora* ID of the person who share their profile # @see Person#author # @return [String] sender ID property :author, :string, xml_name: :sender_handle # @!attribute [r] recipient # The diaspora* ID of the person who will be shared with # @see Validation::Rule::DiasporaId # @return [String] recipient ID property :recipient, :string, xml_name: :recipient_handle def initialize(*) raise "Sending Request is not supported anymore! Use Contact instead!" end # @return [Retraction] instance def self.from_hash(hash) Contact.new(hash) end end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/photo.rb0000644000004100000410000000355013146064271025625 0ustar www-datawww-datamodule DiasporaFederation module Entities # This entity represents a photo and it is federated as a part of a status message. # # @see Validators::PhotoValidator class Photo < Entity # @!attribute [r] guid # A random string of at least 16 chars # @see Validation::Rule::Guid # @return [String] guid property :guid, :string # @!attribute [r] author # The diaspora* ID of the person who uploaded the photo # @see Person#author # @return [String] author diaspora* ID property :author, :string, xml_name: :diaspora_handle # @!attribute [r] public # Points if the photo is visible to everyone or only to some aspects # @return [Boolean] is it public property :public, :boolean, default: false # @!attribute [r] created_at # Photo entity creation time # @return [Time] creation time property :created_at, :timestamp, default: -> { Time.now.utc } # @!attribute [r] remote_photo_path # An url of the photo on a remote server # @return [String] remote photo url property :remote_photo_path, :string # @!attribute [r] remote_photo_name # @return [String] remote photo name property :remote_photo_name, :string # @!attribute [r] text # @return [String] text property :text, :string, optional: true # @!attribute [r] status_message_guid # Guid of a status message this photo belongs to # @see StatusMessage#guid # @return [String] guid property :status_message_guid, :string, optional: true # @!attribute [r] height # Photo height # @return [Integer] height property :height, :integer # @!attribute [r] width # Photo width # @return [Integer] width property :width, :integer end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/event.rb0000644000004100000410000000332213146064271025612 0ustar www-datawww-datamodule DiasporaFederation module Entities # This entity represents an event and it is federated as a part of a status message. # # @see Validators::EventValidator class Event < Entity # @!attribute [r] author # The diaspora* ID of the person who created the event # @see Person#author # @return [String] author diaspora* ID property :author, :string # @!attribute [r] guid # A random string of at least 16 chars # @see Validation::Rule::Guid # @return [String] guid property :guid, :string # @!attribute [r] summary # The summary of the event # @return [String] event summary property :summary, :string # @!attribute [r] description # Description of the event # @return [String] event description property :description, :string, optional: true # @!attribute [r] start # The start time of the event # @return [String] event start property :start, :timestamp # @!attribute [r] end # The end time of the event # @return [String] event end property :end, :timestamp, optional: true # @!attribute [r] all_day # Points if the event is an all day event # @return [Boolean] is it an all day event property :all_day, :boolean, optional: true, default: false # @!attribute [r] timezone # Timezone to which the event is fixed to # @return [String] timezone property :timezone, :string, optional: true # @!attribute [r] location # Location of the event # @return [Entities::Location] location entity :location, Entities::Location, optional: true end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/person.rb0000644000004100000410000000244213146064271026001 0ustar www-datawww-datamodule DiasporaFederation module Entities # This entity contains the base data of a person. # # @see Validators::PersonValidator class Person < Entity # @!attribute [r] guid # This is just the guid. When a user creates an account on a pod, the pod # MUST assign them a guid - a random string of at least 16 chars. # @see Validation::Rule::Guid # @return [String] guid property :guid, :string # @!attribute [r] author # The diaspora* ID of the person # @see Validation::Rule::DiasporaId # @return [String] diaspora* ID # @!attribute [r] diaspora_id # alias for author # @see Person#author # @return [String] diaspora* ID property :author, :string, alias: :diaspora_id, xml_name: :diaspora_handle # @!attribute [r] url # @see Discovery::WebFinger#seed_url # @return [String] link to the pod property :url, :string # @!attribute [r] profile # All profile data of the person # @return [Profile] the profile of the person entity :profile, Entities::Profile # @!attribute [r] exported_key # @see Discovery::HCard#public_key # @return [String] public key property :exported_key, :string end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/contact.rb0000644000004100000410000000213513146064271026125 0ustar www-datawww-datamodule DiasporaFederation module Entities # This entity represents a contact with another person. A user issues it # when they start sharing/following with another user. # # @see Validators::ContactValidator class Contact < Entity # @!attribute [r] author # The diaspora* ID of the person who shares their profile # @see Person#author # @return [String] sender ID property :author, :string # @!attribute [r] recipient # The diaspora* ID of the person who will be shared with # @see Validation::Rule::DiasporaId # @return [String] recipient ID property :recipient, :string # @!attribute [r] following # @return [Boolean] if the author is following the person property :following, :boolean, default: true # @!attribute [r] sharing # @return [Boolean] if the author is sharing with the person property :sharing, :boolean, default: true # @return [String] string representation of this object def to_s "Contact:#{author}:#{recipient}" end end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/signed_retraction.rb0000644000004100000410000000270613146064271030201 0ustar www-datawww-datamodule DiasporaFederation module Entities # This entity represents a claim of deletion of a previously federated # entity of post type. ({Entities::StatusMessage}) # # @see Validators::SignedRetractionValidator # @deprecated will be replaced with {Entities::Retraction} class SignedRetraction < Entity # @!attribute [r] target_guid # Guid of a post to be deleted # @see Retraction#target_guid # @return [String] target guid property :target_guid, :string # @!attribute [r] target_type # A string describing the type of the target # @see Retraction#target_type # @return [String] target type property :target_type, :string # @!attribute [r] author # The diaspora* ID of the person who deletes a post # @see Person#author # @return [String] diaspora* ID property :author, :string, xml_name: :sender_handle # @!attribute [r] author_signature # Contains a signature of the entity using the private key of the author of a post # This signature is mandatory. # @return [String] author signature property :target_author_signature, :string, default: nil def initialize(*) raise "Sending SignedRetraction is not supported anymore! Use Retraction instead!" end # @return [Retraction] instance def self.from_hash(hash) Retraction.from_hash(hash) end end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/message.rb0000644000004100000410000000235513146064271026122 0ustar www-datawww-datamodule DiasporaFederation module Entities # This entity represents a private message exchanged in private conversation. # # @see Validators::MessageValidator class Message < Entity # @!attribute [r] author # The diaspora* ID of the author # @see Person#author # @return [String] diaspora* ID property :author, :string, xml_name: :diaspora_handle # @!attribute [r] guid # A random string of at least 16 chars # @see Validation::Rule::Guid # @return [String] guid property :guid, :string # @!attribute [r] text # Text of the message composed by a user # @return [String] text property :text, :string # @!attribute [r] created_at # Message creation time # @return [Time] creation time property :created_at, :timestamp, default: -> { Time.now.utc } # @!attribute [r] conversation_guid # Guid of a conversation this message belongs to # @see Conversation#guid # @return [String] conversation guid property :conversation_guid, :string # @return [String] string representation of this object def to_s "#{super}:#{conversation_guid}" end end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/participation.rb0000644000004100000410000000335013146064271027340 0ustar www-datawww-datamodule DiasporaFederation module Entities # Participation is sent to subscribe a user on updates for some post. # # @see Validators::Participation class Participation < Entity # @!attribute [r] author # The diaspora* ID of the author # @see Person#author # @return [String] diaspora* ID property :author, :string, xml_name: :diaspora_handle # @!attribute [r] guid # A random string of at least 16 chars # @see Validation::Rule::Guid # @return [String] guid property :guid, :string # @!attribute [r] parent_guid # @see StatusMessage#guid # @return [String] parent guid property :parent_guid, :string # @!attribute [r] parent_type # A string describing a type of the target to subscribe on # Currently only "Post" is supported. # @return [String] parent type property :parent_type, :string, xml_name: :target_type # @return [String] string representation of this object def to_s "#{super}:#{parent_type}:#{parent_guid}" end # Validates that the parent exists and the parent author is local def validate_parent parent = DiasporaFederation.callbacks.trigger(:fetch_related_entity, parent_type, parent_guid) raise ParentNotLocal, "obj=#{self}" unless parent && parent.local end # Validate that the parent is local. # @see Entity.from_hash # @param [Hash] hash entity initialization hash # @return [Entity] instance def self.from_hash(hash) super.tap(&:validate_parent) end # Raised, if the parent is not owned by the receiving pod. class ParentNotLocal < RuntimeError end end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/poll_participation.rb0000644000004100000410000000111513146064271030363 0ustar www-datawww-datamodule DiasporaFederation module Entities # This entity represents a participation in poll, i.e. it is issued when a user votes for an answer in a poll. # # @see Validators::PollParticipationValidator class PollParticipation < Entity # The {PollParticipation} parent is a {Poll} PARENT_TYPE = "Poll".freeze include Relayable # @!attribute [r] poll_answer_guid # Guid of the answer selected by the user # @see PollAnswer#guid # @return [String] poll answer guid property :poll_answer_guid, :string end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/account_deletion.rb0000644000004100000410000000131613146064271030011 0ustar www-datawww-datamodule DiasporaFederation module Entities # This entity is sent when an account was deleted on a remote pod. # # @see Validators::AccountDeletionValidator class AccountDeletion < Entity # @!attribute [r] author # The diaspora* ID of the deleted account # @see Person#author # @return [String] diaspora* ID # @!attribute [r] diaspora_id # Alias for author # @see AccountDeletion#author # @return [String] diaspora* ID property :author, :string, alias: :diaspora_id, xml_name: :diaspora_handle # @return [String] string representation of this object def to_s "AccountDeletion:#{author}" end end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/retraction.rb0000644000004100000410000000365513146064271026654 0ustar www-datawww-datamodule DiasporaFederation module Entities # This entity represents a claim of deletion of a previously federated entity. # # @see Validators::RetractionValidator class Retraction < Entity # @!attribute [r] author # The diaspora* ID of the person who deletes the entity # @see Person#author # @return [String] diaspora* ID property :author, :string, xml_name: :diaspora_handle # @!attribute [r] target_guid # Guid of the entity to be deleted # @return [String] target guid property :target_guid, :string, xml_name: :post_guid # @!attribute [r] target_type # A string describing the type of the target # @return [String] target type property :target_type, :string, xml_name: :type # @!attribute [r] target # Target entity # @return [RelatedEntity] target entity entity :target, Entities::RelatedEntity def sender_valid?(sender) case target_type when "Comment", "Like", "PollParticipation" sender == target.author || sender == target.parent.author else sender == target.author end end # @return [String] string representation of this object def to_s "Retraction:#{target_type}:#{target_guid}" end # @see Entity.from_hash # @return [Retraction] instance def self.from_hash(hash) hash[:target] = fetch_target(hash[:target_type], hash[:target_guid]) new(hash) end private_class_method def self.fetch_target(target_type, target_guid) DiasporaFederation.callbacks.trigger(:fetch_related_entity, target_type, target_guid).tap do |target| raise TargetNotFound, "not found: #{target_type}:#{target_guid}" unless target end end # Raised, if the target of the {Retraction} was not found. class TargetNotFound < RuntimeError end end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/account_migration.rb0000644000004100000410000000474213146064271030205 0ustar www-datawww-datamodule DiasporaFederation module Entities # This entity is sent when a person changes their diaspora* ID (e.g. when a user migration # from one to another pod happens). # # @see Validators::AccountMigrationValidator class AccountMigration < Entity include Signable # @!attribute [r] author # The old diaspora* ID of the person who changes their ID # @see Person#author # @return [String] author diaspora* ID property :author, :string # @!attribute [r] profile # Holds new updated profile of a person, including diaspora* ID # @return [Person] person new data entity :profile, Entities::Profile # @!attribute [r] signature # Signature that validates original and target diaspora* IDs with the new key of person # @return [String] signature property :signature, :string, default: nil # @return [String] string representation of this object def to_s "AccountMigration:#{author}:#{profile.author}" end # Shortcut for calling super method with sensible arguments # # @see DiasporaFederation::Entities::Signable#verify_signature def verify_signature super(profile.author, :signature) end # Calls super and additionally does signature verification for the instantiated entity. # # @see DiasporaFederation::Entity.from_hash def self.from_hash(*args) super.tap(&:verify_signature) end private # @see DiasporaFederation::Entities::Signable#signature_data def signature_data to_s end def enriched_properties super.tap do |hash| hash[:signature] = signature || sign_with_new_key end end # Sign with new user's key # @raise [NewPrivateKeyNotFound] if the new user's private key is not found # @return [String] A Base64 encoded signature of #signature_data with key def sign_with_new_key privkey = DiasporaFederation.callbacks.trigger(:fetch_private_key, profile.author) raise NewPrivateKeyNotFound, "author=#{profile.author} obj=#{self}" if privkey.nil? sign_with_key(privkey).tap do logger.info "event=sign status=complete signature=signature author=#{profile.author} obj=#{self}" end end # Raised, if creating the signature fails, because the new private key of a user was not found class NewPrivateKeyNotFound < RuntimeError end end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities/poll.rb0000644000004100000410000000136013146064271025437 0ustar www-datawww-datamodule DiasporaFederation module Entities # This entity represents a poll and it is federated as an optional part of a status message. # # @see Validators::PollValidator class Poll < Entity # @!attribute [r] guid # A random string of at least 16 chars # @see Validation::Rule::Guid # @return [String] poll guid property :guid, :string # @!attribute [r] question # Text of the question posed by a user # @return [String] question property :question, :string # @!attribute [r] poll_answers # Array of possible answers for the poll # @return [[Entities::PollAnswer]] poll answers entity :poll_answers, [Entities::PollAnswer] end end end diaspora_federation-0.2.1/lib/diaspora_federation/discovery/0000755000004100000410000000000013146064271024327 5ustar www-datawww-datadiaspora_federation-0.2.1/lib/diaspora_federation/discovery/h_card.rb0000644000004100000410000002535313146064271026104 0ustar www-datawww-datamodule DiasporaFederation module Discovery # This class provides the means of generating and parsing account data to and # from the hCard format. # hCard is based on +RFC 2426+ (vCard) which got superseded by +RFC 6350+. # There is a draft for a new h-card format specification, that makes use of # the new vCard standard. # # @note The current implementation contains a huge amount of legacy elements # and classes, that should be removed and cleaned up in later iterations. # # @todo This needs some radical restructuring. The generated HTML is not # correctly nested according to the hCard standard and class names are # partially wrong. Also, apart from that, it's just ugly. # # @example Creating a hCard document from a person hash # hc = HCard.new( # guid: "0123456789abcdef", # nickname: "user", # full_name: "User Name", # seed_url: "https://server.example/", # photo_large_url: "https://server.example/uploads/l.jpg", # photo_medium_url: "https://server.example/uploads/m.jpg", # photo_small_url: "https://server.example/uploads/s.jpg", # public_key: "-----BEGIN PUBLIC KEY-----\nABCDEF==\n-----END PUBLIC KEY-----", # searchable: true, # first_name: "User", # last_name: "Name" # ) # html_string = hc.to_html # # @example Create a HCard instance from an hCard document # hc = HCard.from_html(html_string) # ... # full_name = hc.full_name # ... # # @see http://microformats.org/wiki/hCard "hCard 1.0" # @see http://microformats.org/wiki/h-card "h-card" (draft) # @see http://www.ietf.org/rfc/rfc2426.txt "vCard MIME Directory Profile" (obsolete) # @see http://www.ietf.org/rfc/rfc6350.txt "vCard Format Specification" class HCard < Entity # @!attribute [r] guid # @see Entities::Person#guid # @return [String] guid property :guid, :string # @!attribute [r] nickname # The first part of the diaspora* ID # @return [String] nickname property :nickname, :string, default: nil # @!attribute [r] full_name # @return [String] display name of the user property :full_name, :string # @!attribute [r] url # @deprecated should be changed to the profile url. The pod url is in # the WebFinger (see {WebFinger#seed_url}, will affect older diaspora* # installations). # # @return [String] link to the pod property :url, :string, default: nil # @!attribute [r] public_key # When a user is created on the pod, the pod MUST generate a pgp keypair # for them. This key is used for signing messages. The format is a # DER-encoded PKCS#1 key beginning with the text # "-----BEGIN PUBLIC KEY-----" and ending with "-----END PUBLIC KEY-----". # # @return [String] public key property :public_key, :string # @!attribute [r] photo_large_url # @return [String] url to the big avatar (300x300) property :photo_large_url, :string # @!attribute [r] photo_medium_url # @return [String] url to the medium avatar (100x100) property :photo_medium_url, :string # @!attribute [r] photo_small_url # @return [String] url to the small avatar (50x50) property :photo_small_url, :string # @!attribute [r] first_name # @deprecated We decided to only use one name field, these should be removed # in later iterations (will affect older diaspora* installations). # # @see #full_name # @return [String] first name property :first_name, :string # @!attribute [r] last_name # @deprecated We decided to only use one name field, these should be removed # in later iterations (will affect older diaspora* installations). # # @see #full_name # @return [String] last name property :last_name, :string # @!attribute [r] searchable # @deprecated As this is a simple property, consider move to WebFinger instead # of HCard. vCard has no comparable field for this information, but # Webfinger may declare arbitrary properties (will affect older diaspora* # installations). # # flag if a user is searchable by name # @return [Boolean] searchable flag property :searchable, :boolean # CSS selectors for finding all the hCard fields SELECTORS = { uid: ".uid", nickname: ".nickname", fn: ".fn", given_name: ".given_name", family_name: ".family_name", url: "#pod_location[href]", photo: ".entity_photo .photo[src]", photo_medium: ".entity_photo_medium .photo[src]", photo_small: ".entity_photo_small .photo[src]", key: ".key", searchable: ".searchable" }.freeze # Create the HTML string from the current HCard instance # @return [String] HTML string def to_html builder = create_builder content = builder.doc.at_css("#content_inner") add_simple_property(content, :uid, "uid", @guid) add_simple_property(content, :nickname, "nickname", @nickname) add_simple_property(content, :full_name, "fn", @full_name) add_simple_property(content, :searchable, "searchable", @searchable) add_property(content, :key) do |html| html.pre(@public_key.to_s, class: "key") end # TODO: remove me! ################### add_simple_property(content, :first_name, "given_name", @first_name) add_simple_property(content, :family_name, "family_name", @last_name) ####################################### add_property(content, :url) do |html| html.a(@url.to_s, id: "pod_location", class: "url", rel: "me", href: @url.to_s) end add_photos(content) builder.doc.to_xhtml(indent: 2, indent_text: " ") end # Creates a new HCard instance from the given HTML string # @param html_string [String] HTML string # @return [HCard] HCard instance # @raise [InvalidData] if the HTML string is invalid or incomplete def self.from_html(html_string) doc = parse_html_and_validate(html_string) new( guid: content_from_doc(doc, :uid), nickname: content_from_doc(doc, :nickname), full_name: content_from_doc(doc, :fn), photo_large_url: photo_from_doc(doc, :photo), photo_medium_url: photo_from_doc(doc, :photo_medium), photo_small_url: photo_from_doc(doc, :photo_small), searchable: (content_from_doc(doc, :searchable) == "true"), public_key: content_from_doc(doc, :key), # TODO: remove first_name and last_name! first_name: content_from_doc(doc, :given_name), last_name: content_from_doc(doc, :family_name) ) end private # Creates the base HCard html structure # @return [Nokogiri::HTML::Builder] HTML Builder instance def create_builder Nokogiri::HTML::Builder.new do |html| html.html { html.head { html.meta(charset: "UTF-8") html.title(@full_name) } html.body { html.div(id: "content") { html.h1(@full_name) html.div(id: "content_inner", class: "entity_profile vcard author") { html.h2("User profile") } } } } end end # Add a property to the hCard document. The element will be added to the given # container element and a "definition list" structure will be created around # it. A Nokogiri::HTML::Builder instance will be passed to the given block, # which should be used to add the element(s) containing the property data. # # @param container [Nokogiri::XML::Element] parent element for added property HTML # @param name [Symbol] property name # @yield [Nokogiri::HTML::Builder] html builder def add_property(container, name) Nokogiri::HTML::Builder.with(container) do |html| html.dl(class: "entity_#{name}") { html.dt(name.to_s.capitalize) html.dd { yield html } } end end # Calls {HCard#add_property} for a simple text property # @param container [Nokogiri::XML::Element] parent element # @param name [Symbol] property name # @param class_name [String] HTML class name # @param value [#to_s] property value # @see HCard#add_property def add_simple_property(container, name, class_name, value) add_property(container, name) do |html| html.span(value.to_s, class: class_name) end end # Calls {HCard#add_property} to add the photos # @param container [Nokogiri::XML::Element] parent element # @see HCard#add_property def add_photos(container) add_property(container, :photo) do |html| html.img(class: "photo avatar", width: "300", height: "300", src: @photo_large_url.to_s) end add_property(container, :photo_medium) do |html| html.img(class: "photo avatar", width: "100", height: "100", src: @photo_medium_url.to_s) end add_property(container, :photo_small) do |html| html.img(class: "photo avatar", width: "50", height: "50", src: @photo_small_url.to_s) end end # Make sure some of the most important elements are present in the parsed # HTML document. # @param [LibXML::XML::Document] doc HTML document # @return [Boolean] validation result private_class_method def self.html_document_complete?(doc) !(doc.at_css(SELECTORS[:fn]).nil? || doc.at_css(SELECTORS[:photo]).nil?) end private_class_method def self.parse_html_and_validate(html_string) raise ArgumentError, "hcard html is not a string" unless html_string.instance_of?(String) doc = Nokogiri::HTML::Document.parse(html_string) raise InvalidData, "hcard html incomplete" unless html_document_complete?(doc) doc end private_class_method def self.element_from_doc(doc, selector) doc.at_css(SELECTORS[selector]) end private_class_method def self.content_from_doc(doc, content_selector) element = element_from_doc(doc, content_selector) element.content if element end private_class_method def self.photo_from_doc(doc, photo_selector) element_from_doc(doc, photo_selector)["src"] end end end end diaspora_federation-0.2.1/lib/diaspora_federation/discovery/host_meta.rb0000644000004100000410000000655713146064271026654 0ustar www-datawww-data module DiasporaFederation module Discovery # Generates and parses Host Meta documents. # # This is a minimal implementation of the standard, only to the degree of what # is used for the purposes of the diaspora* protocol. (e.g. WebFinger) # # @example Creating a Host Meta document # doc = HostMeta.from_base_url("https://pod.example.tld/") # doc.to_xml # # @example Parsing a Host Meta document # doc = HostMeta.from_xml(xml_string) # webfinger_tpl = doc.webfinger_template_url # # @see http://tools.ietf.org/html/rfc6415 RFC 6415: "Web Host Metadata" # @see XrdDocument class HostMeta private_class_method :new # Creates a new host-meta instance # @param [String] webfinger_url the webfinger-url def initialize(webfinger_url) @webfinger_url = webfinger_url end # URL fragment to append to the base URL WEBFINGER_SUFFIX = "/.well-known/webfinger.xml?resource={uri}".freeze # Returns the WebFinger URL that was used to build this instance (either from # xml or by giving a base URL). # @return [String] WebFinger template URL def webfinger_template_url @webfinger_url end # Produces the XML string for the Host Meta instance with a +Link+ element # containing the +webfinger_url+. # @return [String] XML string def to_xml doc = XrdDocument.new doc.links << {rel: "lrdd", type: "application/xrd+xml", template: @webfinger_url} doc.to_xml end # Builds a new HostMeta instance and constructs the WebFinger URL from the # given base URL by appending HostMeta::WEBFINGER_SUFFIX. # @param [String, URL] base_url the base-url for the webfinger-url # @return [HostMeta] # @raise [InvalidData] if the webfinger url is malformed def self.from_base_url(base_url) webfinger_url = "#{base_url.to_s.chomp('/')}#{WEBFINGER_SUFFIX}" raise InvalidData, "invalid webfinger url: #{webfinger_url}" unless webfinger_url_valid?(webfinger_url) new(webfinger_url) end # Reads the given Host Meta XML document string and populates the # +webfinger_url+. # @param [String] hostmeta_xml Host Meta XML string # @raise [InvalidData] if the xml or the webfinger url is malformed def self.from_xml(hostmeta_xml) data = XrdDocument.xml_data(hostmeta_xml) raise InvalidData, "received an invalid xml" unless data.key?(:links) webfinger_url = webfinger_url_from_xrd(data) raise InvalidData, "invalid webfinger url: #{webfinger_url}" unless webfinger_url_valid?(webfinger_url) new(webfinger_url) end # Applies some basic sanity-checking to the given URL # @param [String] url validation subject # @return [Boolean] validation result private_class_method def self.webfinger_url_valid?(url) !url.nil? && url.instance_of?(String) && url =~ %r{\Ahttps?:\/\/.*\/.*\{uri\}.*}i end # Gets the webfinger url from an XRD data structure # @param [Hash] data extracted data # @return [String] webfinger url private_class_method def self.webfinger_url_from_xrd(data) link = data[:links].find {|l| l[:rel] == "lrdd" } return link[:template] unless link.nil? end end end end diaspora_federation-0.2.1/lib/diaspora_federation/discovery/web_finger.rb0000644000004100000410000001652313146064271026772 0ustar www-datawww-datamodule DiasporaFederation module Discovery # The WebFinger document used for diaspora* user discovery is based on an # {http://tools.ietf.org/html/draft-jones-appsawg-webfinger older draft of the specification}. # # In the meantime an actual RFC draft has been in development, which should # serve as a base for all future changes of this implementation. # # @example Creating a WebFinger document from a person hash # wf = WebFinger.new( # acct_uri: "acct:user@server.example", # hcard_url: "https://server.example/hcard/users/user", # seed_url: "https://server.example/", # profile_url: "https://server.example/u/user", # atom_url: "https://server.example/public/user.atom", # salmon_url: "https://server.example/receive/users/0123456789abcdef" # ) # xml_string = wf.to_xml # # @example Creating a WebFinger instance from an xml document # wf = WebFinger.from_xml(xml_string) # ... # hcard_url = wf.hcard_url # ... # # @see http://tools.ietf.org/html/draft-jones-appsawg-webfinger "WebFinger" - # current draft # @see http://www.iana.org/assignments/link-relations/link-relations.xhtml # official list of IANA link relations class WebFinger < Entity # @!attribute [r] acct_uri # The Subject element should contain the webfinger address that was asked # for. If it does not, then this webfinger profile MUST be ignored. # @return [String] property :acct_uri, :string # @!attribute [r] hcard_url # @return [String] link to the +hCard+ property :hcard_url, :string # @!attribute [r] seed_url # @return [String] link to the pod property :seed_url, :string # @!attribute [r] profile_url # @return [String] link to the users profile property :profile_url, :string, default: nil # @!attribute [r] atom_url # This atom feed is an Activity Stream of the user's public posts. diaspora* # pods SHOULD publish an Activity Stream of public posts, but there is # currently no requirement to be able to read Activity Streams. # @see http://activitystrea.ms/ Activity Streams specification # # Note that this feed MAY also be made available through the PubSubHubbub # mechanism by supplying a in the atom feed itself. # @return [String] atom feed url property :atom_url, :string, default: nil # @!attribute [r] salmon_url # @note could be nil # @return [String] salmon endpoint url # @see https://cdn.rawgit.com/salmon-protocol/salmon-protocol/master/draft-panzer-salmon-00.html#SMLR # Panzer draft for Salmon, paragraph 3.3 property :salmon_url, :string, default: nil # @!attribute [r] subscribe_url # This url is used to find another user on the home-pod of the user in the webfinger. property :subscribe_url, :string, default: nil # +hcard_url+ link relation REL_HCARD = "http://microformats.org/profile/hcard".freeze # +seed_url+ link relation REL_SEED = "http://joindiaspora.com/seed_location".freeze # +profile_url+ link relation. # @note This might just as well be an +Alias+ instead of a +Link+. REL_PROFILE = "http://webfinger.net/rel/profile-page".freeze # +atom_url+ link relation REL_ATOM = "http://schemas.google.com/g/2010#updates-from".freeze # +salmon_url+ link relation REL_SALMON = "salmon".freeze # +subscribe_url+ link relation REL_SUBSCRIBE = "http://ostatus.org/schema/1.0/subscribe".freeze # Additional WebFinger data # @return [Hash] additional elements attr_reader :additional_data # Initializes a new WebFinger Entity # # @param [Hash] data WebFinger data # @param [Hash] additional_data additional WebFinger data # @option additional_data [Array] :aliases additional aliases # @option additional_data [Hash] :properties properties # @option additional_data [Array] :links additional link elements # @see DiasporaFederation::Entity#initialize def initialize(data, additional_data={}) @additional_data = additional_data super(data) end # Creates the XML string from the current WebFinger instance # @return [String] XML string def to_xml to_xrd.to_xml end def to_json to_xrd.to_json end # Creates a WebFinger instance from the given XML string # @param [String] webfinger_xml WebFinger XML string # @return [WebFinger] WebFinger instance # @raise [InvalidData] if the given XML string is invalid or incomplete def self.from_xml(webfinger_xml) data = parse_xml_and_validate(webfinger_xml) links = data[:links] new( acct_uri: data[:subject], hcard_url: parse_link(links, REL_HCARD), seed_url: parse_link(links, REL_SEED), profile_url: parse_link(links, REL_PROFILE), atom_url: parse_link(links, REL_ATOM), salmon_url: parse_link(links, REL_SALMON), subscribe_url: parse_link_template(links, REL_SUBSCRIBE) ) end # @return [String] string representation of this object def to_s "WebFinger:#{acct_uri}" end private # Parses the XML string to a Hash and does some rudimentary checking on # the data Hash. # @param [String] webfinger_xml WebFinger XML string # @return [Hash] data XML data # @raise [InvalidData] if the given XML string is invalid or incomplete private_class_method def self.parse_xml_and_validate(webfinger_xml) XrdDocument.xml_data(webfinger_xml).tap do |data| valid = data.key?(:subject) && data.key?(:links) raise InvalidData, "webfinger xml is incomplete" unless valid end end def to_xrd XrdDocument.new.tap do |xrd| xrd.subject = acct_uri xrd.aliases.concat(additional_data[:aliases]) if additional_data[:aliases] xrd.properties.merge!(additional_data[:properties]) if additional_data[:properties] add_links_to(xrd) end end def add_links_to(doc) doc.links << {rel: REL_HCARD, type: "text/html", href: hcard_url} doc.links << {rel: REL_SEED, type: "text/html", href: seed_url} add_optional_links_to(doc) doc.links.concat(additional_data[:links]) if additional_data[:links] end def add_optional_links_to(doc) doc.links << {rel: REL_PROFILE, type: "text/html", href: profile_url} if profile_url doc.links << {rel: REL_ATOM, type: "application/atom+xml", href: atom_url} if atom_url doc.links << {rel: REL_SALMON, href: salmon_url} if salmon_url doc.links << {rel: REL_SUBSCRIBE, template: subscribe_url} if subscribe_url end private_class_method def self.find_link(links, rel) links.find {|l| l[:rel] == rel } end private_class_method def self.parse_link(links, rel) element = find_link(links, rel) element ? element[:href] : nil end private_class_method def self.parse_link_template(links, rel) element = find_link(links, rel) element ? element[:template] : nil end end end end diaspora_federation-0.2.1/lib/diaspora_federation/discovery/discovery.rb0000644000004100000410000000626313146064271026672 0ustar www-datawww-datamodule DiasporaFederation module Discovery # This class contains the logic to fetch all data for the given diaspora* ID. class Discovery include DiasporaFederation::Logging # @return [String] the diaspora* ID of the account attr_reader :diaspora_id # Creates a discovery class for the diaspora* ID # @param [String] diaspora_id the diaspora* ID to discover def initialize(diaspora_id) @diaspora_id = clean_diaspora_id(diaspora_id) end # Fetches all metadata for the account and saves it via callback # @return [Person] def fetch_and_save logger.info "Fetch data for #{diaspora_id}" validate_diaspora_id DiasporaFederation.callbacks.trigger(:save_person_after_webfinger, person) logger.info "successfully webfingered #{diaspora_id}" person end private def validate_diaspora_id # Validates if the diaspora* ID matches the diaspora* ID in the webfinger response return if diaspora_id == clean_diaspora_id(webfinger.acct_uri) raise DiscoveryError, "diaspora* ID does not match: Wanted #{diaspora_id} but got" \ " #{clean_diaspora_id(webfinger.acct_uri)}" end def clean_diaspora_id(diaspora_id) diaspora_id.strip.sub("acct:", "").to_s.downcase end def get(url, http_fallback=false) logger.info "Fetching #{url} for #{diaspora_id}" response = HttpClient.get(url) raise "Failed to fetch #{url}: #{response.status}" unless response.success? response.body rescue => e unless http_fallback && url.start_with?("https://") raise DiscoveryError, "Failed to fetch #{url} for #{diaspora_id}: #{e.class}: #{e.message}" end logger.warn "Retry with http: #{url} for #{diaspora_id}: #{e.class}: #{e.message}" url.sub!("https://", "http://") retry end def host_meta_url domain = diaspora_id.split("@")[1] "https://#{domain}/.well-known/host-meta" end def legacy_webfinger_url_from_host_meta # This tries the xrd url with https first, then falls back to http. host_meta = HostMeta.from_xml get(host_meta_url, true) host_meta.webfinger_template_url.gsub("{uri}", "acct:#{diaspora_id}") end def webfinger @webfinger ||= WebFinger.from_xml get(legacy_webfinger_url_from_host_meta) end def hcard @hcard ||= HCard.from_html get(webfinger.hcard_url) end def person @person ||= Entities::Person.new( guid: hcard.guid, diaspora_id: diaspora_id, url: webfinger.seed_url, exported_key: hcard.public_key, profile: profile ) end def profile Entities::Profile.new( diaspora_id: diaspora_id, first_name: hcard.first_name, last_name: hcard.last_name, image_url: hcard.photo_large_url, image_url_medium: hcard.photo_medium_url, image_url_small: hcard.photo_small_url, searchable: hcard.searchable ) end end end end diaspora_federation-0.2.1/lib/diaspora_federation/discovery/xrd_document.rb0000644000004100000410000001450613146064271027355 0ustar www-datawww-datamodule DiasporaFederation module Discovery # This class implements basic handling of XRD documents as far as it is # necessary in the context of the protocols used with diaspora* federation. # # @note {http://tools.ietf.org/html/rfc6415 RFC 6415} recommends that servers # should also offer the JRD format in addition to the XRD representation. # Implementing +XrdDocument#to_json+ and +XrdDocument.json_data+ should # be almost trivial due to the simplicity of the format and the way the data # is stored internally already. See # {http://tools.ietf.org/html/rfc6415#appendix-A RFC 6415, Appendix A} # for a description of the JSON format. # # @example Creating a XrdDocument # doc = XrdDocument.new # doc.expires = DateTime.new(2020, 1, 15, 0, 0, 1) # doc.subject = "http://example.tld/articles/11" # doc.aliases << "http://example.tld/cool_article" # doc.aliases << "http://example.tld/authors/2/articles/3" # doc.properties["http://x.example.tld/ns/version"] = "1.3" # doc.links << { rel: "author", type: "text/html", href: "http://example.tld/authors/2" } # doc.links << { rel: "copyright", template: "http://example.tld/copyright?id={uri}" } # # doc.to_xml # # @example Parsing a XrdDocument # data = XrdDocument.xml_data(xml_string) # # @see http://docs.oasis-open.org/xri/xrd/v1.0/xrd-1.0.html Extensible Resource Descriptor (XRD) Version 1.0 class XrdDocument # xml namespace url XMLNS = "http://docs.oasis-open.org/ns/xri/xrd-1.0".freeze # +Link+ element attributes LINK_ATTRS = %i[rel type href template].freeze # format string for datetime (+Expires+ element) DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ".freeze # The element contains a time value which specifies the instant at # and after which the document has expired and SHOULD NOT be used. # @param [DateTime] value attr_writer :expires # The element contains a URI value which identifies the resource # described by this XRD. # @param [String] value attr_writer :subject # @return [Array] list of alias URIs attr_reader :aliases # @return [Hash mixed>] list of properties. Hash key represents the # +type+ attribute, and the value is the element content attr_reader :properties # @return [Array val>>] list of +Link+ element hashes. Each # hash contains the attributesa and their associated values for the +Link+ # element. attr_reader :links def initialize @aliases = [] @links = [] @properties = {} end # Generates an XML document from the current instance and returns it as string # @return [String] XML document def to_xml Nokogiri::XML::Builder.new(encoding: "UTF-8") {|xml| xml.XRD("xmlns" => XMLNS) { xml.Expires(expires.strftime(DATETIME_FORMAT)) if expires.instance_of?(DateTime) xml.Subject(subject) if !subject.nil? && !subject.empty? add_aliases_to(xml) add_properties_to(xml) add_links_to(xml) } }.to_xml end def to_json { subject: subject, expires: expires, aliases: (aliases if aliases.any?), links: (links if links.any?), properties: (properties if properties.any?) }.reject {|_, v| v.nil? } end # Parse the XRD document from the given string and create a hash containing # the extracted data. # # Small bonus: the hash structure that comes out of this method is the same # as the one used to produce a JRD (JSON Resource Descriptor) or parsing it. # # @param [String] xrd_doc XML string # @return [Hash] extracted data # @raise [InvalidDocument] if the XRD is malformed def self.xml_data(xrd_doc) doc = parse_xrd_document(xrd_doc) {}.tap do |data| exp_elem = doc.at_xpath("xrd:XRD/xrd:Expires", NS) data[:expires] = DateTime.strptime(exp_elem.content, DATETIME_FORMAT) unless exp_elem.nil? subj_elem = doc.at_xpath("xrd:XRD/xrd:Subject", NS) data[:subject] = subj_elem.content unless subj_elem.nil? parse_aliases_from_xml_doc(doc, data) parse_properties_from_xml_doc(doc, data) parse_links_from_xml_doc(doc, data) end end private attr_reader :expires attr_reader :subject NS = {xrd: XMLNS}.freeze def add_aliases_to(xml) aliases.each do |a| next if !a.instance_of?(String) || a.empty? xml.Alias(a.to_s) end end def add_properties_to(xml) properties.each do |type, val| xml.Property(val.to_s, type: type) end end def add_links_to(xml) links.each do |l| attrs = {} LINK_ATTRS.each do |attr| attrs[attr.to_s] = l[attr] if l.key?(attr) end xml.Link(attrs) end end private_class_method def self.parse_xrd_document(xrd_doc) raise ArgumentError unless xrd_doc.instance_of?(String) doc = Nokogiri::XML(xrd_doc) raise InvalidDocument, "Not an XRD document" if !doc.root || doc.root.name != "XRD" doc end private_class_method def self.parse_aliases_from_xml_doc(doc, data) aliases = [] doc.xpath("xrd:XRD/xrd:Alias", NS).each do |node| aliases << node.content end data[:aliases] = aliases unless aliases.empty? end private_class_method def self.parse_properties_from_xml_doc(doc, data) properties = {} doc.xpath("xrd:XRD/xrd:Property", NS).each do |node| properties[node[:type]] = node.children.empty? ? nil : node.content end data[:properties] = properties unless properties.empty? end private_class_method def self.parse_links_from_xml_doc(doc, data) links = [] doc.xpath("xrd:XRD/xrd:Link", NS).each do |node| link = {} LINK_ATTRS.each do |attr| link[attr] = node[attr.to_s] if node.key?(attr.to_s) end links << link end data[:links] = links unless links.empty? end end end end diaspora_federation-0.2.1/lib/diaspora_federation/discovery/exceptions.rb0000644000004100000410000000120313146064271027031 0ustar www-datawww-datamodule DiasporaFederation module Discovery # Raised, if the XML structure is invalid class InvalidDocument < RuntimeError end # Raised, if something is wrong with the webfinger data # # * if the +webfinger_url+ is missing or malformed in {HostMeta.from_base_url} or {HostMeta.from_xml} # * if the parsed XML from {WebFinger.from_xml} is incomplete # * if the html passed to {HCard.from_html} in some way is malformed, invalid or incomplete. class InvalidData < RuntimeError end # Raised, if there is an error while discover a new person class DiscoveryError < RuntimeError end end end diaspora_federation-0.2.1/lib/diaspora_federation/federation.rb0000644000004100000410000000037213146064271024767 0ustar www-datawww-datamodule DiasporaFederation # This module contains the federation logic module Federation end end require "diaspora_federation/federation/fetcher" require "diaspora_federation/federation/receiver" require "diaspora_federation/federation/sender" diaspora_federation-0.2.1/lib/diaspora_federation/parsers/0000755000004100000410000000000013146064271023777 5ustar www-datawww-datadiaspora_federation-0.2.1/lib/diaspora_federation/parsers/base_parser.rb0000644000004100000410000000374113146064271026617 0ustar www-datawww-datamodule DiasporaFederation module Parsers # +BaseParser+ is an abstract class which is used for defining parsers for different # deserialization methods. class BaseParser # @param [Class] entity_type type of DiasporaFederation::Entity that we want to parse with that parser instance def initialize(entity_type) @entity_type = entity_type end # This method is used to parse input with a serialized object data. It returns # a comprehensive data which must be enough to construct a DiasporaFederation::Entity instance. # # Since parser method output is normally passed to a .from_hash method of an entity # as arguments using * operator, the parse method must return an array of a size matching the number # of arguments of .from_hash method of the entity type we link with # @abstract def parse(*) raise NotImplementedError.new("you must override this method when creating your own parser") end private # @param [Symbol] type target type to parse # @param [String] text data as string # @return [String, Boolean, Integer, Time] data def parse_string(type, text) case type when :timestamp begin Time.parse(text).utc rescue nil end when :integer text.to_i if text =~ /\A\d+\z/ when :boolean return true if text =~ /\A(true|t|yes|y|1)\z/i false if text =~ /\A(false|f|no|n|0)\z/i else text end end def assert_parsability_of(entity_class) return if entity_class == entity_type.entity_name raise InvalidRootNode, "'#{entity_class}' can't be parsed by #{entity_type.name}" end attr_reader :entity_type def class_properties entity_type.class_props end # Raised, if the root node doesn't match the class name class InvalidRootNode < RuntimeError end end end end diaspora_federation-0.2.1/lib/diaspora_federation/parsers/relayable_xml_parser.rb0000644000004100000410000000167613146064271030532 0ustar www-datawww-datamodule DiasporaFederation module Parsers # This is a parser of XML serialized object that is normally used for parsing data of relayables. # Explanations about the XML data format can be found # {https://diaspora.github.io/diaspora_federation/federation/xml_serialization.html here}. # Specific features of relayables are described # {https://diaspora.github.io/diaspora_federation/federation/relayable.html here}. # # @see https://diaspora.github.io/diaspora_federation/federation/xml_serialization.html XML Serialization # documentation # @see https://diaspora.github.io/diaspora_federation/federation/relayable.html Relayable documentation class RelayableXmlParser < XmlParser # @see XmlParser#parse # @see BaseParser#parse # @return [Array[2]] comprehensive data for an entity instantiation def parse(*args) hash = super[0] [hash, hash.keys] end end end end diaspora_federation-0.2.1/lib/diaspora_federation/parsers/xml_parser.rb0000644000004100000410000000643513146064271026510 0ustar www-datawww-datamodule DiasporaFederation module Parsers # This is a parser of XML serialized object. # Explanations about the XML data format can be found # {https://diaspora.github.io/diaspora_federation/federation/xml_serialization.html here}. # @see https://diaspora.github.io/diaspora_federation/federation/xml_serialization.html XML Serialization # documentation class XmlParser < BaseParser # @see BaseParser#parse # @param [Nokogiri::XML::Element] root_node root XML node of the XML representation of the entity # @return [Array[1]] comprehensive data for an entity instantiation def parse(root_node) from_xml_sanity_validation(root_node) hash = root_node.element_children.map {|child| xml_name = child.name property = entity_type.find_property_for_xml_name(xml_name) if property type = class_properties[property] value = parse_element_from_node(xml_name, type, root_node) [property, value] else [xml_name, child.text] end }.to_h [hash] end private # @param [String] name property name to parse # @param [Class, Symbol] type target type to parse # @param [Nokogiri::XML::Element] root_node XML node to parse # @return [Object] parsed data def parse_element_from_node(name, type, root_node) if type.instance_of?(Symbol) parse_string_from_node(name, type, root_node) elsif type.instance_of?(Array) parse_array_from_node(type.first, root_node) elsif type.ancestors.include?(Entity) parse_entity_from_node(type, root_node) end end # Create simple entry in data hash # # @param [String] name xml tag to parse # @param [Class, Symbol] type target type to parse # @param [Nokogiri::XML::Element] root_node XML root_node to parse # @return [String] data def parse_string_from_node(name, type, root_node) node = root_node.xpath(name.to_s) node = root_node.xpath(xml_names[name].to_s) if node.empty? parse_string(type, node.first.text) if node.any? end # Create an entry in the data hash for the nested entity # # @param [Class] type target type to parse # @param [Nokogiri::XML::Element] root_node XML node to parse # @return [Entity] parsed child entity def parse_entity_from_node(type, root_node) node = root_node.xpath(type.entity_name) type.from_xml(node.first) if node.any? && node.first.children.any? end # Collect all nested children of that type and create an array in the data hash # # @param [Class] type target type to parse # @param [Nokogiri::XML::Element] root_node XML node to parse # @return [Array] array with parsed child entities def parse_array_from_node(type, root_node) node = root_node.xpath(type.entity_name) node.select {|child| child.children.any? }.map {|child| type.from_xml(child) } unless node.empty? end def from_xml_sanity_validation(root_node) raise ArgumentError, "only Nokogiri::XML::Element allowed" unless root_node.instance_of?(Nokogiri::XML::Element) assert_parsability_of(root_node.name) end end end end diaspora_federation-0.2.1/lib/diaspora_federation/parsers/json_parser.rb0000644000004100000410000000421213146064271026650 0ustar www-datawww-datamodule DiasporaFederation module Parsers # This is a parser of JSON serialized object. JSON object format is defined by # JSON schema which is available at https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json. # TODO: We must publish the schema at a real URL class JsonParser < BaseParser # @see BaseParser#parse # @param [Hash] json_hash A hash acquired by running JSON.parse with JSON serialized entity # @return [Array[1]] comprehensive data for an entity instantiation def parse(json_hash) from_json_sanity_validation(json_hash) parse_entity_data(json_hash["entity_data"]) end private def parse_entity_data(entity_data) hash = entity_data.map {|key, value| property = entity_type.find_property_for_xml_name(key) if property type = entity_type.class_props[property] [property, parse_element_from_value(type, entity_data[key])] else [key, value] end }.to_h [hash] end def parse_element_from_value(type, value) return if value.nil? if %i[integer boolean timestamp].include?(type) && !value.is_a?(String) value elsif type.instance_of?(Symbol) parse_string(type, value) elsif type.instance_of?(Array) raise DeserializationError, "Expected array for #{type}" unless value.respond_to?(:map) value.map {|element| type.first.from_json(element) } elsif type.ancestors.include?(Entity) type.from_json(value) end end def from_json_sanity_validation(json_hash) missing = %w[entity_type entity_data].map {|prop| prop if json_hash[prop].nil? }.compact.join(", ") raise DeserializationError, "Required properties are missing in JSON object: #{missing}" unless missing.empty? assert_parsability_of(json_hash["entity_type"]) end # Raised when the format of the input JSON data doesn't match the parser's expectations class DeserializationError < RuntimeError end end end end diaspora_federation-0.2.1/lib/diaspora_federation/parsers/relayable_json_parser.rb0000644000004100000410000000177313146064271030701 0ustar www-datawww-datamodule DiasporaFederation module Parsers # This is a parser of JSON serialized object, that is normally used for parsing data of relayables. # Assumed format differs from the usual entity by additional "property_order" property which is used to # compute signatures deterministically. # Input JSON for this parser is expected to match "/definitions/relayable" subschema of the JSON schema at # https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json. class RelayableJsonParser < JsonParser # @see JsonParser#parse # @see BaseParser#parse # @return [Array[2]] comprehensive data for an entity instantiation def parse(json_hash) super.push(json_hash["property_order"]) end private def from_json_sanity_validation(json_hash) super return unless json_hash["property_order"].nil? raise DeserializationError, "Required property is missing in JSON object: property_order" end end end end diaspora_federation-0.2.1/lib/diaspora_federation/logging.rb0000644000004100000410000000172313146064271024276 0ustar www-datawww-datamodule DiasporaFederation # Logging module for the diaspora* federation # # It uses the logging-gem if available. module Logging # Add +logger+ also as class method when included # @param [Class] klass the class into which the module is included def self.included(klass) klass.extend(self) end private # Get the logger for this class # # Use the logging-gem if available, else use a default logger. def logger @logger ||= begin # Use logging-gem if available return ::Logging::Logger[self] if defined?(::Logging::Logger) # Use rails logger if running in rails and no logging-gem is available return ::Rails.logger if defined?(::Rails) # fallback logger @logger = Logger.new(STDOUT) @logger.level = Logger::INFO @logger end end end end diaspora_federation-0.2.1/lib/diaspora_federation/version.rb0000644000004100000410000000011513146064271024327 0ustar www-datawww-datamodule DiasporaFederation # the gem version VERSION = "0.2.1".freeze end diaspora_federation-0.2.1/lib/diaspora_federation/parsers.rb0000644000004100000410000000077413146064271024334 0ustar www-datawww-datamodule DiasporaFederation # This namespace contains parsers which are used to deserialize federation entities # objects from supported formats (XML, JSON) to objects of DiasporaFederation::Entity # classes module Parsers end end require "diaspora_federation/parsers/base_parser" require "diaspora_federation/parsers/json_parser" require "diaspora_federation/parsers/xml_parser" require "diaspora_federation/parsers/relayable_json_parser" require "diaspora_federation/parsers/relayable_xml_parser" diaspora_federation-0.2.1/lib/diaspora_federation/callbacks.rb0000644000004100000410000000327513146064271024573 0ustar www-datawww-datamodule DiasporaFederation # Callbacks are used to communicate with the application. They are called to # fetch data and after data is received. class Callbacks # Initializes a new Callbacks object with the event-keys that need to be defined # # @example # Callbacks.new %i( # some_event # another_event # ) # # @param [Hash] events event keys def initialize(events) @events = events @handlers = {} end # Defines a callback # # @example # callbacks.on :some_event do |arg1| # # do something # end # # @param [Symbol] event the event key # @param [Proc] callback the callback block # @raise [ArgumentError] if the event key is undefined or has already a handler def on(event, &callback) raise ArgumentError, "Undefined event #{event}" unless @events.include? event raise ArgumentError, "Already defined event #{event}" if @handlers.has_key? event @handlers[event] = callback end # Triggers a callback # # @example # callbacks.trigger :some_event, "foo" # # @param [Symbol] event the event key # @return [Object] the return-value of the callback # @raise [ArgumentError] if the event key is undefined def trigger(event, *args) raise ArgumentError, "Undefined event #{event}" unless @events.include? event @handlers[event].call(*args) end # Checks if all callbacks are defined # @return [Boolean] def definition_complete? missing_handlers.empty? end # Returns all undefined callbacks # @return [Hash] callback keys def missing_handlers @events - @handlers.keys end end end diaspora_federation-0.2.1/lib/diaspora_federation/salmon.rb0000644000004100000410000000116413146064271024140 0ustar www-datawww-datamodule DiasporaFederation # This module contains a diaspora*-specific implementation of parts of the # {http://www.salmon-protocol.org/ Salmon Protocol}. module Salmon # XML namespace url XMLNS = "https://joindiaspora.com/protocol".freeze end end require "base64" require "diaspora_federation/salmon/aes" require "diaspora_federation/salmon/exceptions" require "diaspora_federation/salmon/xml_payload" require "diaspora_federation/salmon/magic_envelope" require "diaspora_federation/salmon/encrypted_magic_envelope" require "diaspora_federation/salmon/slap" require "diaspora_federation/salmon/encrypted_slap" diaspora_federation-0.2.1/lib/diaspora_federation/properties_dsl.rb0000644000004100000410000001260513146064271025707 0ustar www-datawww-datamodule DiasporaFederation # Provides a simple DSL for specifying {Entity} properties during class # definition. # # @example # property :prop # property :optional, default: false # property :dynamic_default, default: -> { Time.now } # property :another_prop, xml_name: :another_name # property :original_prop, alias: :alias_prop # entity :nested, NestedEntity # entity :multiple, [OtherEntity] module PropertiesDSL # @return [Hash] hash of declared entity properties def class_props @class_props ||= {} end # Define a generic (string-type) property # @param [Symbol] name property name # @param [Symbol] type property type # @param [Hash] opts further options # @option opts [Object, #call] :default a default value, making the # property optional # @option opts [Symbol] :xml_name another name used for xml generation def property(name, type, opts={}) raise InvalidType unless property_type_valid?(type) define_property name, type, opts end # Define a property that should contain another Entity or an array of # other Entities # @param [Symbol] name property name # @param [Entity, Array] type Entity subclass or # Array with exactly one Entity subclass constant inside # @param [Hash] opts further options # @option opts [Object, #call] :default a default value, making the # property optional def entity(name, type, opts={}) raise InvalidType unless entity_type_valid?(type) define_property name, type, opts end # Return array of missing required property names # @return [Array] missing required property names def missing_props(args) class_props.keys - default_props.keys - optional_props - args.keys end def optional_props @optional_props ||= [] end # Return a new hash of default values, with dynamic values # resolved on each call # @return [Hash] default values def default_values optional_props.map {|name| [name, nil] }.to_h.merge(default_props).map {|name, prop| [name, prop.respond_to?(:call) ? prop.call : prop] }.to_h end # @param [Hash] data entity data # @return [Hash] hash with resolved aliases def resolv_aliases(data) data.map {|name, value| if class_prop_aliases.has_key? name prop_name = class_prop_aliases[name] raise InvalidData, "only use '#{name}' OR '#{prop_name}'" if data.has_key? prop_name [prop_name, value] else [name, value] end }.to_h end # @return [Symbol] alias for the xml-generation/parsing # @deprecated def xml_names @xml_names ||= {} end # Finds a property by +xml_name+ or +name+ # @param [String] xml_name name of the property from the received xml # @return [Hash] the property data def find_property_for_xml_name(xml_name) class_props.keys.find {|name| name.to_s == xml_name || xml_names[name].to_s == xml_name } end private # @deprecated def determine_xml_name(name, type, opts={}) if !type.instance_of?(Symbol) && opts.has_key?(:xml_name) raise ArgumentError, "xml_name is not supported for nested entities" end if type.instance_of?(Symbol) if opts.has_key? :xml_name raise InvalidName, "invalid xml_name" unless name_valid?(opts[:xml_name]) opts[:xml_name] else name end elsif type.instance_of?(Array) type.first.entity_name.to_sym elsif type.ancestors.include?(Entity) type.entity_name.to_sym end end def define_property(name, type, opts={}) raise InvalidName unless name_valid?(name) class_props[name] = type optional_props << name if opts[:optional] default_props[name] = opts[:default] if opts.has_key? :default xml_names[name] = determine_xml_name(name, type, opts) instance_eval { attr_reader name } define_alias(name, opts[:alias]) if opts.has_key? :alias end # Checks if the name is a +Symbol+ or a +String+ # @param [String, Symbol] name the name to check # @return [Boolean] def name_valid?(name) name.instance_of?(Symbol) end def property_type_valid?(type) %i[string integer boolean timestamp].include?(type) end # Checks if the type extends {Entity} # @param [Class] type the type to check # @return [Boolean] def entity_type_valid?(type) [type].flatten.all? {|type| type.respond_to?(:ancestors) && type.ancestors.include?(Entity) } end def default_props @default_props ||= {} end # Returns all alias mappings # @return [Hash] alias properties def class_prop_aliases @class_prop_aliases ||= {} end # @param [Symbol] name property name # @param [Symbol] alias_name alias name def define_alias(name, alias_name) class_prop_aliases[alias_name] = name # rubocop:disable Style/Alias instance_eval { alias_method alias_name, name } # rubocop:enable Style/Alias end # Raised, if the name is of an unexpected type class InvalidName < RuntimeError end # Raised, if the type is of an unexpected type class InvalidType < RuntimeError end # Raised, if the data contains property twice (with name AND alias) class InvalidData < RuntimeError end end end diaspora_federation-0.2.1/lib/diaspora_federation/entities.rb0000644000004100000410000000327113146064271024474 0ustar www-datawww-datamodule DiasporaFederation # This namespace contains all the entities used to encapsulate data that is # passed around in the diaspora* network as part of the federation protocol. # # All entities must be defined in this namespace. Otherwise the XML # de-serialization will fail. module Entities end end require "diaspora_federation/entities/related_entity" # abstract types require "diaspora_federation/entities/post" require "diaspora_federation/entities/signable" require "diaspora_federation/entities/relayable" # types require "diaspora_federation/entities/profile" require "diaspora_federation/entities/person" require "diaspora_federation/entities/contact" require "diaspora_federation/entities/account_deletion" require "diaspora_federation/entities/account_migration" require "diaspora_federation/entities/participation" require "diaspora_federation/entities/like" require "diaspora_federation/entities/comment" require "diaspora_federation/entities/poll_answer" require "diaspora_federation/entities/poll" require "diaspora_federation/entities/poll_participation" require "diaspora_federation/entities/location" require "diaspora_federation/entities/event" require "diaspora_federation/entities/event_participation" require "diaspora_federation/entities/photo" require "diaspora_federation/entities/status_message" require "diaspora_federation/entities/reshare" require "diaspora_federation/entities/message" require "diaspora_federation/entities/conversation" require "diaspora_federation/entities/retraction" # deprecated require "diaspora_federation/entities/request" require "diaspora_federation/entities/signed_retraction" require "diaspora_federation/entities/relayable_retraction" diaspora_federation-0.2.1/lib/diaspora_federation/federation/0000755000004100000410000000000013146064271024440 5ustar www-datawww-datadiaspora_federation-0.2.1/lib/diaspora_federation/federation/sender/0000755000004100000410000000000013146064271025720 5ustar www-datawww-datadiaspora_federation-0.2.1/lib/diaspora_federation/federation/sender/hydra_wrapper.rb0000644000004100000410000001035713146064271031122 0ustar www-datawww-datarequire "typhoeus" module DiasporaFederation module Federation module Sender # A wrapper for [Typhoeus::Hydra] # # Uses parallel http requests to send out the salmon-messages class HydraWrapper include Logging # Hydra default opts # @return [Hash] hydra opts def self.hydra_opts @hydra_opts ||= { followlocation: true, maxredirs: DiasporaFederation.http_redirect_limit, timeout: DiasporaFederation.http_timeout, method: :post, verbose: DiasporaFederation.http_verbose, cainfo: DiasporaFederation.certificate_authorities, forbid_reuse: true } end def self.xml_headers @xml_headers ||= { "Content-Type" => "application/magic-envelope+xml", "User-Agent" => DiasporaFederation.http_user_agent } end def self.json_headers @json_headers ||= { "Content-Type" => "application/json", "User-Agent" => DiasporaFederation.http_user_agent } end # Create a new instance for a message # # @param [String] sender_id sender diaspora-ID # @param [String] obj_str object string representation for logging (e.g. type@guid) def initialize(sender_id, obj_str) @sender_id = sender_id @obj_str = obj_str @urls_to_retry = [] end # Prepares and inserts a public MagicEnvelope job into the hydra queue # @param [String] url the receive-url for the xml # @param [String] xml MagicEnvelope xml def insert_magic_env_request(url, xml) insert_job(url, HydraWrapper.hydra_opts.merge(body: xml, headers: HydraWrapper.xml_headers)) end # Prepares and inserts a private encrypted MagicEnvelope job into the hydra queue # @param [String] url the receive-url for the message # @param [String] json encrypted MagicEnvelope json def insert_enc_magic_env_request(url, json) insert_job(url, HydraWrapper.hydra_opts.merge(body: json, headers: HydraWrapper.json_headers)) end # Sends all queued messages # @return [Array] urls to retry def send hydra.run @urls_to_retry end private # Prepares and inserts job into the hydra queue # @param [String] url the receive-url for the message # @param [Hash] options request options def insert_job(url, options) request = Typhoeus::Request.new(url, options) prepare_request(request) hydra.queue(request) end # @return [Typhoeus::Hydra] hydra def hydra @hydra ||= Typhoeus::Hydra.new(max_concurrency: DiasporaFederation.http_concurrency) end # Logic for after complete # @param [Typhoeus::Request] request def prepare_request(request) request.on_complete do |response| success = validate_response_and_update_pod(request, response) log_line = "success=#{success} sender=#{@sender_id} obj=#{@obj_str} url=#{response.effective_url} " \ "message=#{response.return_code} code=#{response.response_code} time=#{response.total_time}" if success logger.info(log_line) else logger.warn(log_line) @urls_to_retry << request.url end end end def validate_response_and_update_pod(request, response) url = URI.parse(request.url) effective_url = URI.parse(response.effective_url) same_host = url.host == effective_url.host (response.success? && same_host).tap do |success| pod_url = (success ? effective_url : url).tap {|uri| uri.path = "/" }.to_s status = same_host ? status_from_response(response) : :redirected_to_other_hostname DiasporaFederation.callbacks.trigger(:update_pod, pod_url, status) end end def status_from_response(response) response.return_code == :ok ? response.response_code : response.return_code end end end end end diaspora_federation-0.2.1/lib/diaspora_federation/federation/fetcher.rb0000644000004100000410000000265213146064271026412 0ustar www-datawww-datamodule DiasporaFederation module Federation # This module is for fetching entities from other pods. module Fetcher # Fetches a public entity from a remote pod # @param [String] author the diaspora* ID of the author of the entity # @param [Symbol, String] entity_type snake_case version of the entity class # @param [String] guid guid of the entity to fetch def self.fetch_public(author, entity_type, guid) url = DiasporaFederation.callbacks.trigger( :fetch_person_url_to, author, "/fetch/#{entity_name(entity_type)}/#{guid}" ) response = HttpClient.get(url) raise "Failed to fetch #{url}: #{response.status}" unless response.success? magic_env_xml = Nokogiri::XML(response.body).root magic_env = Salmon::MagicEnvelope.unenvelop(magic_env_xml) Receiver::Public.new(magic_env).receive rescue => e raise NotFetchable, "Failed to fetch #{entity_type}:#{guid} from #{author}: #{e.class}: #{e.message}" end private_class_method def self.entity_name(class_name) return class_name if class_name =~ /\A[a-z]*(_[a-z]*)*\z/ raise DiasporaFederation::Entity::UnknownEntity, class_name unless Entities.const_defined?(class_name) class_name.gsub(/(.)([A-Z])/, '\1_\2').downcase end # Raised, if the entity is not fetchable class NotFetchable < RuntimeError end end end end diaspora_federation-0.2.1/lib/diaspora_federation/federation/receiver.rb0000644000004100000410000000425213146064271026574 0ustar www-datawww-datamodule DiasporaFederation module Federation # This module parses and receives entities. module Receiver extend Logging # Receive a public message # @param [String] data message to receive # @param [Boolean] legacy use old slap parser def self.receive_public(data, legacy=false) magic_env = if legacy Salmon::Slap.from_xml(data) else magic_env_xml = Nokogiri::XML(data).root Salmon::MagicEnvelope.unenvelop(magic_env_xml) end Public.new(magic_env).receive rescue => e logger.error "failed to receive public message: #{e.class}: #{e.message}" logger.debug "received data:\n#{data}" raise e end # Receive a private message # @param [String] data message to receive # @param [OpenSSL::PKey::RSA] recipient_private_key recipient private key to decrypt the message # @param [Object] recipient_id the identifier to persist the entity for the correct user, # see +receive_entity+ callback # @param [Boolean] legacy use old slap parser def self.receive_private(data, recipient_private_key, recipient_id, legacy=false) raise ArgumentError, "no recipient key provided" unless recipient_private_key.instance_of?(OpenSSL::PKey::RSA) magic_env = if legacy Salmon::EncryptedSlap.from_xml(data, recipient_private_key) else magic_env_xml = Salmon::EncryptedMagicEnvelope.decrypt(data, recipient_private_key) Salmon::MagicEnvelope.unenvelop(magic_env_xml) end Private.new(magic_env, recipient_id).receive rescue => e logger.error "failed to receive private message for #{recipient_id}: #{e.class}: #{e.message}" logger.debug "received data:\n#{data}" raise e end end end end require "diaspora_federation/federation/receiver/exceptions" require "diaspora_federation/federation/receiver/abstract_receiver" require "diaspora_federation/federation/receiver/public" require "diaspora_federation/federation/receiver/private" diaspora_federation-0.2.1/lib/diaspora_federation/federation/sender.rb0000644000004100000410000000250013146064271026242 0ustar www-datawww-datamodule DiasporaFederation module Federation # Federation logic to send messages to other pods module Sender # Send a public message to all urls # # @param [String] sender_id sender diaspora-ID # @param [String] obj_str object string representation for logging (e.g. type@guid) # @param [Array] urls receive-urls from pods # @param [String] xml salmon-xml # @return [Array] url to retry def self.public(sender_id, obj_str, urls, xml) hydra = HydraWrapper.new(sender_id, obj_str) urls.each {|url| hydra.insert_magic_env_request(url, xml) } hydra.send end # Send a private message to receive-urls # # @param [String] sender_id sender diaspora-ID # @param [String] obj_str object string representation for logging (e.g. type@guid) # @param [Hash] targets Hash with receive-urls (key) of peoples with encrypted salmon-xml for them (value) # @return [Hash] targets to retry def self.private(sender_id, obj_str, targets) hydra = HydraWrapper.new(sender_id, obj_str) targets.each {|url, json| hydra.insert_enc_magic_env_request(url, json) } hydra.send.map {|url| [url, targets[url]] }.to_h end end end end require "diaspora_federation/federation/sender/hydra_wrapper" diaspora_federation-0.2.1/lib/diaspora_federation/federation/receiver/0000755000004100000410000000000013146064271026244 5ustar www-datawww-datadiaspora_federation-0.2.1/lib/diaspora_federation/federation/receiver/public.rb0000644000004100000410000000064113146064271030050 0ustar www-datawww-datamodule DiasporaFederation module Federation module Receiver # Receiver for public entities class Public < AbstractReceiver private def validate super raise NotPublic if entity_can_be_public_but_it_is_not? end def entity_can_be_public_but_it_is_not? entity.respond_to?(:public) && !entity.public end end end end end diaspora_federation-0.2.1/lib/diaspora_federation/federation/receiver/abstract_receiver.rb0000644000004100000410000000263713146064271032270 0ustar www-datawww-datamodule DiasporaFederation module Federation module Receiver # Common functionality for receivers class AbstractReceiver include Logging # Creates a new receiver # @param [MagicEnvelope] magic_envelope the received magic envelope # @param [Object] recipient_id the identifier of the recipient of a private message def initialize(magic_envelope, recipient_id=nil) @entity = magic_envelope.payload @sender = magic_envelope.sender @recipient_id = recipient_id end # Validates and receives the entity def receive validate_and_receive rescue => e logger.error "failed to receive #{entity}" raise e end private attr_reader :entity, :sender, :recipient_id def validate_and_receive validate DiasporaFederation.callbacks.trigger(:receive_entity, entity, sender, recipient_id) logger.info "successfully received #{entity} from person #{sender}#{" for #{recipient_id}" if recipient_id}" end def validate raise InvalidSender, "invalid sender: #{sender}" unless sender_valid? end def sender_valid? if entity.respond_to?(:sender_valid?) entity.sender_valid?(sender) else sender == entity.author end end end end end end diaspora_federation-0.2.1/lib/diaspora_federation/federation/receiver/private.rb0000644000004100000410000000044413146064271030245 0ustar www-datawww-datamodule DiasporaFederation module Federation module Receiver # Receiver for private entities class Private < AbstractReceiver private def validate raise RecipientRequired if recipient_id.nil? super end end end end end diaspora_federation-0.2.1/lib/diaspora_federation/federation/receiver/exceptions.rb0000644000004100000410000000076013146064271030755 0ustar www-datawww-datamodule DiasporaFederation module Federation module Receiver # Raised, if the sender of the {Salmon::MagicEnvelope} is not allowed to send the entity. class InvalidSender < RuntimeError end # Raised, if receiving a private message without recipient. class RecipientRequired < RuntimeError end # Raised, if receiving a message with public receiver which is not public but should be. class NotPublic < RuntimeError end end end end diaspora_federation-0.2.1/lib/diaspora_federation/discovery.rb0000644000004100000410000000104013146064271024647 0ustar www-datawww-datamodule DiasporaFederation # This module provides the namespace for the various classes implementing # WebFinger and other protocols used for metadata discovery on remote servers # in the diaspora* network. module Discovery end end require "diaspora_federation/discovery/exceptions" require "diaspora_federation/discovery/xrd_document" require "diaspora_federation/discovery/host_meta" require "diaspora_federation/discovery/web_finger" require "diaspora_federation/discovery/h_card" require "diaspora_federation/discovery/discovery" diaspora_federation-0.2.1/lib/diaspora_federation/validators/0000755000004100000410000000000013146064271024470 5ustar www-datawww-datadiaspora_federation-0.2.1/lib/diaspora_federation/validators/message_validator.rb0000644000004100000410000000056213146064271030511 0ustar www-datawww-datamodule DiasporaFederation module Validators # This validates a {Entities::Message}. class MessageValidator < Validation::Validator include Validation rule :author, %i[not_empty diaspora_id] rule :guid, :guid rule :conversation_guid, :guid rule :text, [:not_empty, length: {maximum: 65_535}] end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/participation_validator.rb0000644000004100000410000000057113146064271031733 0ustar www-datawww-datamodule DiasporaFederation module Validators # This validates a {Entities::Participation}. class ParticipationValidator < Validation::Validator include Validation rule :author, %i[not_empty diaspora_id] rule :guid, :guid rule :parent_guid, :guid rule :parent_type, [:not_empty, regular_expression: {regex: /\APost\z/}] end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/contact_validator.rb0000644000004100000410000000052613146064271030520 0ustar www-datawww-datamodule DiasporaFederation module Validators # This validates a {Entities::Contact}. class ContactValidator < Validation::Validator include Validation rule :author, %i[not_empty diaspora_id] rule :recipient, %i[not_empty diaspora_id] rule :following, :boolean rule :sharing, :boolean end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/location_validator.rb0000644000004100000410000000036313146064271030674 0ustar www-datawww-datamodule DiasporaFederation module Validators # This validates a {Entities::Location}. class LocationValidator < Validation::Validator include Validation rule :lat, :not_empty rule :lng, :not_empty end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/relayable_validator.rb0000644000004100000410000000114413146064271031022 0ustar www-datawww-datamodule DiasporaFederation module Validators # This is included to validatros which validate entities which include {Entities::Relayable}. module RelayableValidator # When this module is included in a Validator child it adds rules for relayable validation. # @param [Validation::Validator] validator the validator in which it is included def self.included(validator) validator.class_eval do rule :author, %i[not_empty diaspora_id] rule :guid, :guid rule :parent_guid, :guid rule :parent, :not_nil end end end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/h_card_validator.rb0000644000004100000410000000144513146064271030306 0ustar www-datawww-datamodule DiasporaFederation module Validators # This validates a {Discovery::HCard}. # # @note class HCardValidator < Validation::Validator include Validation rule :guid, :guid # The name must not contain a semicolon because of mentions. # @{ ; } rule :full_name, regular_expression: {regex: /\A[^;]{,70}\z/} rule :first_name, regular_expression: {regex: /\A[^;]{,32}\z/} rule :last_name, regular_expression: {regex: /\A[^;]{,32}\z/} # this urls can be relative rule :photo_large_url, [:not_nil, URI: [:path]] rule :photo_medium_url, [:not_nil, URI: [:path]] rule :photo_small_url, [:not_nil, URI: [:path]] rule :public_key, :public_key rule :searchable, :boolean end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/comment_validator.rb0000644000004100000410000000045013146064271030523 0ustar www-datawww-datamodule DiasporaFederation module Validators # This validates a {Entities::Comment}. class CommentValidator < Validation::Validator include Validation include RelayableValidator rule :text, [:not_empty, length: {maximum: 65_535}] end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/account_deletion_validator.rb0000644000004100000410000000036713146064271032407 0ustar www-datawww-datamodule DiasporaFederation module Validators # This validates a {Entities::AccountDeletion}. class AccountDeletionValidator < Validation::Validator include Validation rule :author, %i[not_empty diaspora_id] end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/account_migration_validator.rb0000644000004100000410000000043013146064271032564 0ustar www-datawww-datamodule DiasporaFederation module Validators # This validates a {Entities::AccountMigration}. class AccountMigrationValidator < Validation::Validator include Validation rule :author, %i[not_empty diaspora_id] rule :profile, :not_nil end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/related_entity_validator.rb0000644000004100000410000000045413146064271032101 0ustar www-datawww-datamodule DiasporaFederation module Validators # This validates a {Entities::RelatedEntity}. class RelatedEntityValidator < Validation::Validator include Validation rule :author, %i[not_empty diaspora_id] rule :local, :boolean rule :public, :boolean end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/photo_validator.rb0000644000004100000410000000102013146064271030204 0ustar www-datawww-datamodule DiasporaFederation module Validators # This validates a {Entities::Photo}. class PhotoValidator < Validation::Validator include Validation rule :guid, :guid rule :author, %i[not_empty diaspora_id] rule :public, :boolean rule :remote_photo_path, :not_empty rule :remote_photo_name, :not_empty rule :status_message_guid, guid: {nilable: true} rule :text, length: {maximum: 65_535} rule :height, :numeric rule :width, :numeric end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/poll_participation_validator.rb0000644000004100000410000000042313146064271032755 0ustar www-datawww-datamodule DiasporaFederation module Validators # This validates a {Entities::PollParticipation}. class PollParticipationValidator < Validation::Validator include Validation include RelayableValidator rule :poll_answer_guid, :guid end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/poll_answer_validator.rb0000644000004100000410000000042013146064271031403 0ustar www-datawww-datamodule DiasporaFederation module Validators # This validates a {Entities::PollAnswer}. class PollAnswerValidator < Validation::Validator include Validation rule :guid, :guid rule :answer, [:not_empty, length: {maximum: 255}] end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/reshare_validator.rb0000644000004100000410000000056013146064271030514 0ustar www-datawww-datamodule DiasporaFederation module Validators # This validates a {Entities::Reshare}. class ReshareValidator < Validation::Validator include Validation rule :root_author, %i[not_empty diaspora_id] rule :root_guid, :guid rule :author, %i[not_empty diaspora_id] rule :guid, :guid rule :public, :boolean end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/web_finger_validator.rb0000644000004100000410000000106113146064271031167 0ustar www-datawww-datamodule DiasporaFederation module Validators # This validates a {Discovery::WebFinger}. # # @note It does not validate the guid and public key, because it will be # removed in the webfinger. class WebFingerValidator < Validation::Validator include Validation rule :acct_uri, :not_empty rule :hcard_url, [:not_nil, URI: %i[host path]] rule :seed_url, %i[not_nil URI] rule :profile_url, URI: %i[host path] rule :atom_url, URI: %i[host path] rule :salmon_url, URI: %i[host path] end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/event_participation_validator.rb0000644000004100000410000000050613146064271033132 0ustar www-datawww-datamodule DiasporaFederation module Validators # This validates a {Entities::EventParticipation}. class EventParticipationValidator < Validation::Validator include Validation include RelayableValidator rule :status, regular_expression: {regex: /\A(accepted|declined|tentative)\z/} end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/person_validator.rb0000644000004100000410000000054613146064271030375 0ustar www-datawww-datamodule DiasporaFederation module Validators # This validates a {Entities::Person}. class PersonValidator < Validation::Validator include Validation rule :guid, :guid rule :author, %i[not_empty diaspora_id] rule :url, %i[not_nil URI] rule :profile, :not_nil rule :exported_key, :public_key end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/event_validator.rb0000644000004100000410000000101113146064271030174 0ustar www-datawww-datamodule DiasporaFederation module Validators # This validates a {Entities::Event}. class EventValidator < Validation::Validator include Validation rule :author, %i[not_empty diaspora_id] rule :guid, :guid rule :summary, [:not_empty, length: {maximum: 255}] rule :description, length: {maximum: 65_535} rule :start, :not_nil rule :all_day, :boolean rule :timezone, regular_expression: {regex: %r{\A[A-Za-z_-]{,14}(/[A-Za-z_-]{,14}){1,2}\z}} end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/status_message_validator.rb0000644000004100000410000000056513146064271032117 0ustar www-datawww-datamodule DiasporaFederation module Validators # This validates a {Entities::StatusMessage}. class StatusMessageValidator < Validation::Validator include Validation rule :author, %i[not_empty diaspora_id] rule :guid, :guid rule :text, length: {maximum: 65_535} rule :photos, :not_nil rule :public, :boolean end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/retraction_validator.rb0000644000004100000410000000051613146064271031236 0ustar www-datawww-datamodule DiasporaFederation module Validators # This validates a {Entities::Retraction}. class RetractionValidator < Validation::Validator include Validation rule :author, %i[not_empty diaspora_id] rule :target_guid, :guid rule :target_type, :not_empty rule :target, :not_nil end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/poll_validator.rb0000644000004100000410000000046513146064271030035 0ustar www-datawww-datamodule DiasporaFederation module Validators # This validates a {Entities::Poll}. class PollValidator < Validation::Validator include Validation rule :guid, :guid rule :question, [:not_empty, length: {maximum: 255}] rule :poll_answers, length: {minimum: 2} end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/conversation_validator.rb0000644000004100000410000000063713146064271031602 0ustar www-datawww-datamodule DiasporaFederation module Validators # This validates a {Entities::Conversation}. class ConversationValidator < Validation::Validator include Validation rule :author, %i[not_empty diaspora_id] rule :guid, :guid rule :subject, [:not_empty, length: {maximum: 255}] rule :participants, diaspora_id_count: {maximum: 20} rule :messages, :not_nil end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/rules/0000755000004100000410000000000013146064271025622 5ustar www-datawww-datadiaspora_federation-0.2.1/lib/diaspora_federation/validators/rules/not_nil.rb0000644000004100000410000000066013146064271027613 0ustar www-datawww-datamodule Validation module Rule # Validates that a property is not +nil+ class NotNil # The error key for this rule # @return [Symbol] error key def error_key :not_nil end # Determines if value is not nil def valid_value?(value) !value.nil? end # This rule has no params. # @return [Hash] params def params {} end end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/rules/tag_count.rb0000644000004100000410000000172513146064271030137 0ustar www-datawww-datamodule Validation module Rule # Rule for validating the number of tags in a string. # Only the "#" characters will be counted. # The string can be nil. class TagCount # This rule must have a +maximum+ param. # @return [Hash] params attr_reader :params # Creates a new rule for a maximum tag count validation # @param [Hash] params # @option params [Integer] :maximum maximum allowed tag count def initialize(params) unless params.include?(:maximum) && params[:maximum].is_a?(Integer) raise ArgumentError, "A number has to be specified for :maximum" end @params = params end # The error key for this rule # @return [Symbol] error key def error_key :tag_count end # Determines if value doesn't have more than +maximum+ tags def valid_value?(value) value.nil? || value.count("#") <= params[:maximum] end end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/rules/birthday.rb0000644000004100000410000000164013146064271027756 0ustar www-datawww-datarequire "date" module Validation module Rule # Birthday validation rule # # Valid is: # * nil or an empty +String+ # * a +Date+ object # * a +String+ with the format "yyyy-mm-dd" and is a valid +Date+, example: 2015-07-25 class Birthday # The error key for this rule # @return [Symbol] error key def error_key :birthday end # Determines if value is a valid birthday date. def valid_value?(value) return true if value.nil? || (value.is_a?(String) && value.empty?) return true if value.is_a? Date if value =~ /[0-9]{4}\-[0-9]{2}\-[0-9]{2}/ date_field = value.split("-").map(&:to_i) return Date.valid_civil?(date_field[0], date_field[1], date_field[2]) end false end # This rule has no params. # @return [Hash] params def params {} end end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/rules/diaspora_id.rb0000644000004100000410000000242313146064271030426 0ustar www-datawww-datamodule Validation module Rule # diaspora* ID validation rule # # A simple rule to validate the base structure of diaspora* IDs. class DiasporaId # The Regex for a valid diaspora* ID DIASPORA_ID = begin letter = "a-zA-Z" digit = "0-9" hexadecimal = "[a-fA-F#{digit}]" username = "[#{letter}#{digit}\\-\\_\\.]+" hostname_part = "[#{letter}#{digit}\\-]" hostname = "#{hostname_part}+([.]#{hostname_part}*)*" ipv4 = "(?:[#{digit}]{1,3}\\.){3}[#{digit}]{1,3}" ipv6 = "\\[(?:#{hexadecimal}{0,4}:){0,7}#{hexadecimal}{1,4}\\]" ip_addr = "(?:#{ipv4}|#{ipv6})" domain = "(?:#{hostname}|#{ip_addr})" port = "(:[#{digit}]+)?" addr_spec = "(#{username}\\@#{domain}#{port})?" /\A#{addr_spec}\z/u end # The error key for this rule # @return [Symbol] error key def error_key :diaspora_id end # Determines if value is a valid diaspora* ID def valid_value?(value) value.nil? || !DIASPORA_ID.match(value).nil? end # This rule has no params. # @return [Hash] params def params {} end end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/rules/boolean.rb0000644000004100000410000000156113146064271027571 0ustar www-datawww-datamodule Validation module Rule # Boolean validation rule # # Valid is: # * a +String+: "true", "false", "t", "f", "yes", "no", "y", "n", "1", "0" # * a +Integer+: 1 or 0 # * a +Boolean+: true or false class Boolean # The error key for this rule # @return [Symbol] error key def error_key :boolean end # Determines if value is a valid +boolean+ def valid_value?(value) return false if value.nil? if value.is_a?(String) true if value =~ /\A(true|false|t|f|yes|no|y|n|1|0)\z/i elsif value.is_a?(Integer) true if [1, 0].include?(value) elsif [true, false].include?(value) true else false end end # This rule has no params. # @return [Hash] params def params {} end end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/rules/public_key.rb0000644000004100000410000000166313146064271030303 0ustar www-datawww-datamodule Validation module Rule # Public key validation rule # # A valid key must: # * start with "-----BEGIN PUBLIC KEY-----" and end with "-----END PUBLIC KEY-----" # or # * start with "-----BEGIN RSA PUBLIC KEY-----" and end with "-----END RSA PUBLIC KEY-----" class PublicKey # The error key for this rule # @return [Symbol] error key def error_key :public_key end # Determines if value is a valid public key def valid_value?(value) !value.nil? && ( (value.strip.start_with?("-----BEGIN PUBLIC KEY-----") && value.strip.end_with?("-----END PUBLIC KEY-----")) || (value.strip.start_with?("-----BEGIN RSA PUBLIC KEY-----") && value.strip.end_with?("-----END RSA PUBLIC KEY-----")) ) end # This rule has no params. # @return [Hash] params def params {} end end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/rules/guid.rb0000644000004100000410000000211313146064271027074 0ustar www-datawww-datamodule Validation module Rule # GUID validation rule # # Valid is a +String+ that is at least 16 and at most 255 chars long. It contains only: # * Letters: a-z # * Numbers: 0-9 # * Special chars: '-', '_', '@', '.' and ':' class Guid # This rule can have a +nilable+ param. # @return [Hash] params attr_reader :params # Creates a new rule for guid validation # @param [Hash] params # @option params [Boolean] :nilable guid allowed to be nil def initialize(params={}) if params.include?(:nilable) && !params[:nilable].is_a?(TrueClass) && !params[:nilable].is_a?(FalseClass) raise ArgumentError, ":nilable needs to be a boolean" end @params = params end # The error key for this rule # @return [Symbol] error key def error_key :guid end # Determines if value is a valid +GUID+ def valid_value?(value) params[:nilable] && value.nil? || value.is_a?(String) && value.downcase =~ /\A[0-9a-z\-_@.:]{16,255}\z/ end end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/rules/diaspora_id_count.rb0000644000004100000410000000210313146064271031631 0ustar www-datawww-datamodule Validation module Rule # Rule for validating the number of diaspora* IDs in a string. # The evaluated string is split at ";" and the result will be counted. class DiasporaIdCount # This rule must have a +maximum+ param. # @return [Hash] params attr_reader :params # Creates a new rule for a maximum diaspora* ID count validation # @param [Hash] params # @option params [Integer] :maximum maximum allowed id count def initialize(params) unless params.include?(:maximum) && params[:maximum].is_a?(Integer) raise ArgumentError, "A number has to be specified for :maximum" end @params = params end # The error key for this rule # @return [Symbol] error key def error_key :diaspora_id_count end def valid_value?(value) ids = value.split(";") return false unless ids.count <= params[:maximum] ids.each do |id| return false if DiasporaId::DIASPORA_ID.match(id).nil? end true end end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/like_validator.rb0000644000004100000410000000045613146064271030013 0ustar www-datawww-datamodule DiasporaFederation module Validators # This validates a {Entities::Like}. class LikeValidator < Validation::Validator include Validation include RelayableValidator rule :parent_type, [:not_empty, regular_expression: {regex: /\A(Post|Comment)\z/}] end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators/profile_validator.rb0000644000004100000410000000163413146064271030526 0ustar www-datawww-datamodule DiasporaFederation module Validators # This validates a {Entities::Profile}. class ProfileValidator < Validation::Validator include Validation rule :author, :diaspora_id # The name must not contain a semicolon because of mentions. # @{ ; } rule :first_name, regular_expression: {regex: /\A[^;]{,32}\z/} rule :last_name, regular_expression: {regex: /\A[^;]{,32}\z/} # These urls can be relative. rule :image_url, URI: [:path] rule :image_url_medium, URI: [:path] rule :image_url_small, URI: [:path] rule :birthday, :birthday rule :gender, length: {maximum: 255} rule :bio, length: {maximum: 65_535} rule :location, length: {maximum: 255} rule :searchable, :boolean rule :public, :boolean rule :nsfw, :boolean rule :tag_string, tag_count: {maximum: 5} end end end diaspora_federation-0.2.1/lib/diaspora_federation/http_client.rb0000644000004100000410000000220413146064271025160 0ustar www-datawww-datarequire "faraday" require "faraday_middleware/response/follow_redirects" module DiasporaFederation # A wrapper for {https://github.com/lostisland/faraday Faraday} # # @see Discovery::Discovery # @see Federation::Fetcher class HttpClient # Perform a GET request # # @param [String] uri the URI # @return [Faraday::Response] the response def self.get(uri) connection.get(uri) end # Gets the Faraday connection # # @return [Faraday::Connection] the response def self.connection create_default_connection unless @connection @connection.dup end private_class_method def self.create_default_connection options = { request: {timeout: DiasporaFederation.http_timeout}, ssl: {ca_file: DiasporaFederation.certificate_authorities} } @connection = Faraday::Connection.new(options) do |builder| builder.use FaradayMiddleware::FollowRedirects, limit: DiasporaFederation.http_redirect_limit builder.adapter Faraday.default_adapter end @connection.headers["User-Agent"] = DiasporaFederation.http_user_agent end end end diaspora_federation-0.2.1/lib/diaspora_federation/validators.rb0000644000004100000410000000555713146064271025031 0ustar www-datawww-datarequire "validation" require "validation/rule/regular_expression" require "validation/rule/length" require "validation/rule/not_empty" require "validation/rule/uri" require "validation/rule/numeric" # +valid+ gem namespace module Validation # This module contains custom validation rules for various data field types. # That includes types for which there are no provided rules by the +valid+ gem # or types that are very specific to diaspora* federation and need special handling. # The rules are used inside the {DiasporaFederation::Validators validator classes} # to perform basic sanity-checks on {DiasporaFederation::Entities federation entities}. module Rule end end require "diaspora_federation/validators/rules/birthday" require "diaspora_federation/validators/rules/boolean" require "diaspora_federation/validators/rules/diaspora_id" require "diaspora_federation/validators/rules/diaspora_id_count" require "diaspora_federation/validators/rules/guid" require "diaspora_federation/validators/rules/not_nil" require "diaspora_federation/validators/rules/public_key" require "diaspora_federation/validators/rules/tag_count" module DiasporaFederation # Validators to perform basic sanity-checks on {DiasporaFederation::Entities federation entities}. # # The Validators are mapped with the entities by name. The naming schema # is "Validator". module Validators end end require "diaspora_federation/validators/related_entity_validator" # abstract types require "diaspora_federation/validators/relayable_validator" # types require "diaspora_federation/validators/account_deletion_validator" require "diaspora_federation/validators/account_migration_validator" require "diaspora_federation/validators/comment_validator" require "diaspora_federation/validators/contact_validator" require "diaspora_federation/validators/conversation_validator" require "diaspora_federation/validators/event_participation_validator" require "diaspora_federation/validators/event_validator" require "diaspora_federation/validators/h_card_validator" require "diaspora_federation/validators/like_validator" require "diaspora_federation/validators/location_validator" require "diaspora_federation/validators/message_validator" require "diaspora_federation/validators/participation_validator" require "diaspora_federation/validators/person_validator" require "diaspora_federation/validators/photo_validator" require "diaspora_federation/validators/poll_answer_validator" require "diaspora_federation/validators/poll_participation_validator" require "diaspora_federation/validators/poll_validator" require "diaspora_federation/validators/profile_validator" require "diaspora_federation/validators/reshare_validator" require "diaspora_federation/validators/retraction_validator" require "diaspora_federation/validators/status_message_validator" require "diaspora_federation/validators/web_finger_validator" diaspora_federation-0.2.1/lib/diaspora_federation/salmon/0000755000004100000410000000000013146064271023611 5ustar www-datawww-datadiaspora_federation-0.2.1/lib/diaspora_federation/salmon/slap.rb0000644000004100000410000000374713146064271025110 0ustar www-datawww-datamodule DiasporaFederation module Salmon # +Slap+ provides class methods to create unencrypted Slap XML from payload # data and parse incoming XML into a Slap instance. # # A diaspora* flavored magic-enveloped XML message looks like the following: # # # #
# {author} #
# {magic_envelope} #
# # @example Parsing a Salmon Slap # entity = Slap.from_xml(slap_xml).payload # # @deprecated class Slap # Namespaces NS = {d: Salmon::XMLNS, me: MagicEnvelope::XMLNS}.freeze # Parses an unencrypted Salmon XML string and returns a new instance of # {MagicEnvelope} with the XML data. # # @param [String] slap_xml Salmon XML # # @return [MagicEnvelope] magic envelope instance with payload and sender # # @raise [ArgumentError] if the argument is not a String # @raise [MissingAuthor] if the +author_id+ element is missing from the XML # @raise [MissingMagicEnvelope] if the +me:env+ element is missing from the XML def self.from_xml(slap_xml) raise ArgumentError unless slap_xml.instance_of?(String) doc = Nokogiri::XML(slap_xml) author_elem = doc.at_xpath("d:diaspora/d:header/d:author_id", Slap::NS) raise MissingAuthor if author_elem.nil? || author_elem.content.empty? sender = author_elem.content MagicEnvelope.unenvelop(magic_env_from_doc(doc), sender) end # Parses the magic envelop from the document. # # @param [Nokogiri::XML::Document] doc Salmon XML Document private_class_method def self.magic_env_from_doc(doc) doc.at_xpath("d:diaspora/me:env", Slap::NS).tap do |env| raise MissingMagicEnvelope if env.nil? end end end end end diaspora_federation-0.2.1/lib/diaspora_federation/salmon/xml_payload.rb0000644000004100000410000000300013146064271026440 0ustar www-datawww-datamodule DiasporaFederation module Salmon # +XmlPayload+ provides methods to wrap a XML-serialized {Entity} inside a # common XML structure that will become the payload for federation messages. # # The wrapper looks like so: # # # {data} # # # # (The +post+ element is there for historic reasons...) # @deprecated module XmlPayload # Extracts the Entity XML from the wrapping XML structure, parses the entity # XML and returns a new instance of the Entity that was packed inside the # given payload. # # @param [Nokogiri::XML::Element] xml payload XML root node # @return [Entity] re-constructed Entity instance # @raise [ArgumentError] if the argument is not an # {http://www.rubydoc.info/gems/nokogiri/Nokogiri/XML/Element Nokogiri::XML::Element} # @raise [UnknownEntity] if the class for the entity contained inside the # XML can't be found def self.unpack(xml) raise ArgumentError, "only Nokogiri::XML::Element allowed" unless xml.instance_of?(Nokogiri::XML::Element) data = xml_wrapped?(xml) ? xml.at_xpath("post/*[1]") : xml Entity.entity_class(data.name).from_xml(data) end # @param [Nokogiri::XML::Element] element private_class_method def self.xml_wrapped?(element) (element.name == "XML" && !element.at_xpath("post").nil? && !element.at_xpath("post").children.empty?) end end end end diaspora_federation-0.2.1/lib/diaspora_federation/salmon/encrypted_slap.rb0000644000004100000410000001106213146064271027152 0ustar www-datawww-datarequire "json" module DiasporaFederation module Salmon # +EncryptedSlap+ provides class methods for generating and parsing encrypted # Slaps. (In principle the same as {Slap}, but with encryption.) # # The basic encryption mechanism used here is based on the knowledge that # asymmetrical encryption is slow and symmetrical encryption is fast. Keeping in # mind that a message we want to de-/encrypt may greatly vary in length, # performance considerations must play a part of this scheme. # # A diaspora*-flavored encrypted magic-enveloped XML message looks like the following: # # # # {encrypted_header} # {magic_envelope with encrypted data} # # # The encrypted header is encoded in JSON like this (when in plain text): # # { # "aes_key" => "...", # "ciphertext" => "..." # } # # +aes_key+ is encrypted using the recipients public key, and contains the AES # +key+ and +iv+ used to encrypt the +ciphertext+ also encoded as JSON. # # { # "key" => "...", # "iv" => "..." # } # # +ciphertext+, once decrypted, contains the +author_id+, +aes_key+ and +iv+ # relevant to the decryption of the data in the magic_envelope and the # verification of its signature. # # The decrypted cyphertext has this XML structure: # # # {iv} # {aes_key} # {author_id} # # # Finally, before decrypting the magic envelope payload, the signature should # first be verified. # # @example Parsing a Salmon Slap # recipient_privkey = however_you_retrieve_the_recipients_private_key() # entity = EncryptedSlap.from_xml(slap_xml, recipient_privkey).payload # # @deprecated class EncryptedSlap < Slap # Creates a {MagicEnvelope} instance from the data within the given XML string # containing an encrypted payload. # # @param [String] slap_xml encrypted Salmon xml # @param [OpenSSL::PKey::RSA] privkey recipient private_key for decryption # # @return [MagicEnvelope] magic envelope instance with payload and sender # # @raise [ArgumentError] if any of the arguments is of the wrong type # @raise [MissingHeader] if the +encrypted_header+ element is missing in the XML # @raise [MissingMagicEnvelope] if the +me:env+ element is missing in the XML def self.from_xml(slap_xml, privkey) raise ArgumentError unless slap_xml.instance_of?(String) && privkey.instance_of?(OpenSSL::PKey::RSA) doc = Nokogiri::XML(slap_xml) header_elem = doc.at_xpath("d:diaspora/d:encrypted_header", Slap::NS) raise MissingHeader if header_elem.nil? header = header_data(header_elem.content, privkey) sender = header[:author_id] cipher_params = {key: Base64.decode64(header[:aes_key]), iv: Base64.decode64(header[:iv])} MagicEnvelope.unenvelop(magic_env_from_doc(doc), sender, cipher_params) end # Decrypts and reads the data from the encrypted XML header # @param [String] data base64 encoded, encrypted header data # @param [OpenSSL::PKey::RSA] privkey private key for decryption # @return [Hash] { iv: "...", aes_key: "...", author_id: "..." } private_class_method def self.header_data(data, privkey) header_elem = decrypt_header(data, privkey) raise InvalidHeader unless header_elem.name == "decrypted_header" iv = header_elem.at_xpath("iv").content key = header_elem.at_xpath("aes_key").content author_id = header_elem.at_xpath("author_id").content {iv: iv, aes_key: key, author_id: author_id} end # Decrypts the xml header # @param [String] data base64 encoded, encrypted header data # @param [OpenSSL::PKey::RSA] privkey private key for decryption # @return [Nokogiri::XML::Element] header xml document private_class_method def self.decrypt_header(data, privkey) cipher_header = JSON.parse(Base64.decode64(data)) key = JSON.parse(privkey.private_decrypt(Base64.decode64(cipher_header["aes_key"]))) xml = AES.decrypt(cipher_header["ciphertext"], Base64.decode64(key["key"]), Base64.decode64(key["iv"])) Nokogiri::XML(xml).root end end end end diaspora_federation-0.2.1/lib/diaspora_federation/salmon/magic_envelope.rb0000644000004100000410000002274613146064271027126 0ustar www-datawww-datamodule DiasporaFederation module Salmon # Represents a Magic Envelope for diaspora* federation messages # # When generating a Magic Envelope, an instance of this class is created and # the contents are specified on initialization. Optionally, the payload can be # encrypted ({MagicEnvelope#encrypt!}), before the XML is returned # ({MagicEnvelope#envelop}). # # The generated XML appears like so: # # # {data} # base64url # RSA-SHA256 # {signature} # # # When parsing the XML of an incoming Magic Envelope {MagicEnvelope.unenvelop} # is used. # # @see https://cdn.rawgit.com/salmon-protocol/salmon-protocol/master/draft-panzer-magicsig-01.html class MagicEnvelope include Logging # Encoding used for the payload data ENCODING = "base64url".freeze # Algorithm used for signing the payload data ALGORITHM = "RSA-SHA256".freeze # Mime type describing the payload data DATA_TYPE = "application/xml".freeze # Digest instance used for signing DIGEST = OpenSSL::Digest::SHA256.new # XML namespace url XMLNS = "http://salmon-protocol.org/ns/magic-env".freeze # The payload entity of the magic envelope # @return [Entity] payload entity attr_reader :payload # The sender of the magic envelope # @return [String] diaspora-ID of the sender attr_reader :sender # Creates a new instance of MagicEnvelope. # # @param [Entity] payload Entity instance # @param [String] sender diaspora-ID of the sender # @raise [ArgumentError] if either argument is not of the right type def initialize(payload, sender=nil) raise ArgumentError unless payload.is_a?(Entity) @payload = payload @sender = sender end # Builds the XML structure for the magic envelope, inserts the {ENCODING} # encoded data and signs the envelope using {DIGEST}. # # @param [OpenSSL::PKey::RSA] privkey private key used for signing # @return [Nokogiri::XML::Element] XML root node def envelop(privkey) raise ArgumentError unless privkey.instance_of?(OpenSSL::PKey::RSA) build_xml {|xml| xml["me"].env("xmlns:me" => XMLNS) { xml["me"].data(Base64.urlsafe_encode64(payload_data), type: DATA_TYPE) xml["me"].encoding(ENCODING) xml["me"].alg(ALGORITHM) xml["me"].sig(Base64.urlsafe_encode64(sign(privkey)), key_id) } } end # Extracts the entity encoded in the magic envelope data, if the signature # is valid. If +cipher_params+ is given, also attempts to decrypt the payload first. # # Does some sanity checking to avoid bad surprises... # # @see XmlPayload#unpack # @see AES#decrypt # # @param [Nokogiri::XML::Element] magic_env XML root node of a magic envelope # @param [String] sender diaspora* ID of the sender or nil # @param [Hash] cipher_params hash containing the key and iv for # AES-decrypting previously encrypted data. E.g.: { iv: "...", key: "..." } # # @return [Entity] reconstructed entity instance # # @raise [ArgumentError] if any of the arguments is of invalid type # @raise [InvalidEnvelope] if the envelope XML structure is malformed # @raise [InvalidSignature] if the signature can't be verified # @raise [InvalidDataType] if the data is missing or unsupported # @raise [InvalidEncoding] if the data is wrongly encoded or encoding is missing # @raise [InvalidAlgorithm] if the algorithm is missing or doesn't match def self.unenvelop(magic_env, sender=nil, cipher_params=nil) raise ArgumentError unless magic_env.instance_of?(Nokogiri::XML::Element) validate_envelope(magic_env) validate_type(magic_env) validate_encoding(magic_env) validate_algorithm(magic_env) sender ||= sender(magic_env) raise InvalidSignature unless signature_valid?(magic_env, sender) data = read_and_decrypt_data(magic_env, cipher_params) logger.debug "unenvelop message from #{sender}:\n#{data}" new(XmlPayload.unpack(Nokogiri::XML(data).root), sender) end private # The payload data as string # @return [String] payload data def payload_data @payload_data ||= payload.to_xml.to_xml.strip.tap do |data| logger.debug "send payload:\n#{data}" end end def key_id sender ? {key_id: Base64.urlsafe_encode64(sender)} : {} end # Builds the xml root node of the magic envelope. # # @yield [xml] Invokes the block with the # {http://www.rubydoc.info/gems/nokogiri/Nokogiri/XML/Builder Nokogiri::XML::Builder} # @return [Nokogiri::XML::Element] XML root node def build_xml Nokogiri::XML::Builder.new(encoding: "UTF-8") {|xml| yield xml }.doc end # Creates the signature for all fields according to specification # # @param [OpenSSL::PKey::RSA] privkey private key used for signing # @return [String] the signature def sign(privkey) subject = MagicEnvelope.send(:sig_subject, [payload_data, DATA_TYPE, ENCODING, ALGORITHM]) privkey.sign(DIGEST, subject) end # @param [Nokogiri::XML::Element] env magic envelope XML # @raise [InvalidEnvelope] if the envelope XML structure is malformed private_class_method def self.validate_envelope(env) raise InvalidEnvelope unless env.instance_of?(Nokogiri::XML::Element) && env.name == "env" validate_element(env, "me:data") validate_element(env, "me:sig") end # @param [Nokogiri::XML::Element] env magic envelope XML # @param [String] xpath the element to validate # @raise [InvalidEnvelope] if the element is missing or empty private_class_method def self.validate_element(env, xpath) element = env.at_xpath(xpath) raise InvalidEnvelope, "missing #{xpath}" unless element raise InvalidEnvelope, "empty #{xpath}" if element.content.empty? end # @param [Nokogiri::XML::Element] env magic envelope XML # @param [String] sender diaspora* ID of the sender or nil # @return [Boolean] private_class_method def self.signature_valid?(env, sender) subject = sig_subject([Base64.urlsafe_decode64(env.at_xpath("me:data").content), env.at_xpath("me:data")["type"], env.at_xpath("me:encoding").content, env.at_xpath("me:alg").content]) sender_key = DiasporaFederation.callbacks.trigger(:fetch_public_key, sender) raise SenderKeyNotFound unless sender_key sig = Base64.urlsafe_decode64(env.at_xpath("me:sig").content) sender_key.verify(DIGEST, sig, subject) end # Reads the +key_id+ from the magic envelope. # @param [Nokogiri::XML::Element] env magic envelope XML # @return [String] diaspora* ID of the sender private_class_method def self.sender(env) key_id = env.at_xpath("me:sig")["key_id"] raise InvalidEnvelope, "no key_id" unless key_id # TODO: move to `envelope_valid?` Base64.urlsafe_decode64(key_id) end # Constructs the signature subject. # The given array should consist of the data, data_type (mimetype), encoding # and the algorithm. # @param [Array] data_arr # @return [String] signature subject private_class_method def self.sig_subject(data_arr) data_arr.map {|i| Base64.urlsafe_encode64(i) }.join(".") end # @param [Nokogiri::XML::Element] magic_env magic envelope XML # @raise [InvalidDataType] if the data is missing or unsupported private_class_method def self.validate_type(magic_env) type = magic_env.at_xpath("me:data")["type"] raise InvalidDataType, "missing data type" if type.nil? raise InvalidDataType, "invalid data type: #{type}" unless type == DATA_TYPE end # @param [Nokogiri::XML::Element] magic_env magic envelope XML # @raise [InvalidEncoding] if the data is wrongly encoded or encoding is missing private_class_method def self.validate_encoding(magic_env) enc = magic_env.at_xpath("me:encoding") raise InvalidEncoding, "missing encoding" unless enc raise InvalidEncoding, "invalid encoding: #{enc.content}" unless enc.content == ENCODING end # @param [Nokogiri::XML::Element] magic_env magic envelope XML # @raise [InvalidAlgorithm] if the algorithm is missing or doesn't match private_class_method def self.validate_algorithm(magic_env) alg = magic_env.at_xpath("me:alg") raise InvalidAlgorithm, "missing algorithm" unless alg raise InvalidAlgorithm, "invalid algorithm: #{alg.content}" unless alg.content == ALGORITHM end # @param [Nokogiri::XML::Element] magic_env magic envelope XML # @param [Hash] cipher_params hash containing the key and iv # @return [String] data private_class_method def self.read_and_decrypt_data(magic_env, cipher_params) data = Base64.urlsafe_decode64(magic_env.at_xpath("me:data").content) data = AES.decrypt(data, cipher_params[:key], cipher_params[:iv]) unless cipher_params.nil? data end end end end diaspora_federation-0.2.1/lib/diaspora_federation/salmon/exceptions.rb0000644000004100000410000000256613146064271026330 0ustar www-datawww-datamodule DiasporaFederation module Salmon # Raised, if the element containing the Magic Envelope is missing from the XML # @deprecated class MissingMagicEnvelope < RuntimeError end # Raised, if the element containing the author is empty. # @deprecated class MissingAuthor < RuntimeError end # Raised, if the element containing the header is missing from the XML # @deprecated class MissingHeader < RuntimeError end # Raised, if the decrypted header has an unexpected XML structure # @deprecated class InvalidHeader < RuntimeError end # Raised, if failed to fetch the public key of the sender of the received message class SenderKeyNotFound < RuntimeError end # Raised, if the Magic Envelope XML structure is malformed. class InvalidEnvelope < RuntimeError end # Raised, if the calculated signature doesn't match the one contained in the # Magic Envelope. class InvalidSignature < RuntimeError end # Raised, if the parsed Magic Envelope specifies an unhandled data type. class InvalidDataType < RuntimeError end # Raised, if the parsed Magic Envelope specifies an unhandled algorithm. class InvalidAlgorithm < RuntimeError end # Raised, if the parsed Magic Envelope specifies an unhandled encoding. class InvalidEncoding < RuntimeError end end end diaspora_federation-0.2.1/lib/diaspora_federation/salmon/encrypted_magic_envelope.rb0000644000004100000410000000445013146064271031173 0ustar www-datawww-datamodule DiasporaFederation module Salmon # This is a simple crypt-wrapper for {MagicEnvelope}. # # The wrapper is JSON with the following structure: # # { # "aes_key": "...", # "encrypted_magic_envelope": "..." # } # # +aes_key+ is encrypted using the recipients public key, and contains the AES # +key+ and +iv+ as JSON: # # { # "key": "...", # "iv": "..." # } # # +encrypted_magic_envelope+ is encrypted using the +key+ and +iv+ from +aes_key+. # Once decrypted it contains the {MagicEnvelope} xml: # # # ... # # # All JSON-values (+aes_key+, +encrypted_magic_envelope+, +key+ and +iv+) are # base64 encoded. module EncryptedMagicEnvelope # Generates a new random AES key and encrypts the {MagicEnvelope} with it. # Then encrypts the AES key with the receivers public key. # @param [Nokogiri::XML::Element] magic_env XML root node of a magic envelope # @param [OpenSSL::PKey::RSA] pubkey recipient public_key # @return [String] json string def self.encrypt(magic_env, pubkey) key = AES.generate_key_and_iv encrypted_env = AES.encrypt(magic_env.to_xml, key[:key], key[:iv]) encoded_key = key.map {|k, v| [k, Base64.strict_encode64(v)] }.to_h encrypted_key = Base64.strict_encode64(pubkey.public_encrypt(JSON.generate(encoded_key))) JSON.generate(aes_key: encrypted_key, encrypted_magic_envelope: encrypted_env) end # Decrypts the AES key with the private key of the receiver and decrypts the # encrypted {MagicEnvelope} with it. # @param [String] encrypted_env json string with aes_key and encrypted_magic_envelope # @param [OpenSSL::PKey::RSA] privkey private key for decryption # @return [Nokogiri::XML::Element] decrypted magic envelope xml def self.decrypt(encrypted_env, privkey) encrypted_json = JSON.parse(encrypted_env) encoded_key = JSON.parse(privkey.private_decrypt(Base64.decode64(encrypted_json["aes_key"]))) key = encoded_key.map {|k, v| [k, Base64.decode64(v)] }.to_h xml = AES.decrypt(encrypted_json["encrypted_magic_envelope"], key["key"], key["iv"]) Nokogiri::XML(xml).root end end end end diaspora_federation-0.2.1/lib/diaspora_federation/salmon/aes.rb0000644000004100000410000000421213146064271024705 0ustar www-datawww-datamodule DiasporaFederation module Salmon # Class for AES encryption and decryption class AES # OpenSSL aes cipher definition CIPHER = "AES-256-CBC".freeze # Generates a random AES key and initialization vector # @return [Hash] { key: "...", iv: "..." } def self.generate_key_and_iv cipher = OpenSSL::Cipher.new(CIPHER) {key: cipher.random_key, iv: cipher.random_iv} end # Encrypts the given data with an AES cipher defined by the given key # and iv and returns the resulting ciphertext base64 strict_encoded. # @param [String] data plain input # @param [String] key AES key # @param [String] iv AES initialization vector # @return [String] base64 encoded ciphertext # @raise [ArgumentError] if any of the arguments is missing or not the correct type def self.encrypt(data, key, iv) raise ArgumentError unless data.instance_of?(String) && key.instance_of?(String) && iv.instance_of?(String) cipher = OpenSSL::Cipher.new(CIPHER) cipher.encrypt cipher.key = key cipher.iv = iv ciphertext = cipher.update(data) + cipher.final Base64.strict_encode64(ciphertext) end # Decrypts the given ciphertext with an AES cipher defined by the given key # and iv. +ciphertext+ is expected to be base64 encoded # @param [String] ciphertext input data # @param [String] key AES key # @param [String] iv AES initialization vector # @return [String] decrypted plain message # @raise [ArgumentError] if any of the arguments is missing or not the correct type def self.decrypt(ciphertext, key, iv) raise ArgumentError unless ciphertext.instance_of?(String) && key.instance_of?(String) && iv.instance_of?(String) decipher = OpenSSL::Cipher.new(CIPHER) decipher.decrypt decipher.key = key decipher.iv = iv decipher.update(Base64.decode64(ciphertext)) + decipher.final end end end end diaspora_federation-0.2.1/lib/diaspora_federation.rb0000644000004100000410000002125013146064271022645 0ustar www-datawww-datarequire "nokogiri" require "openssl" require "diaspora_federation/version" require "diaspora_federation/logging" require "diaspora_federation/callbacks" require "diaspora_federation/properties_dsl" require "diaspora_federation/entity" require "diaspora_federation/validators" require "diaspora_federation/http_client" require "diaspora_federation/entities" require "diaspora_federation/parsers" require "diaspora_federation/discovery" require "diaspora_federation/salmon" require "diaspora_federation/federation" # diaspora* federation library module DiasporaFederation extend Logging @callbacks = Callbacks.new %i[ fetch_person_for_webfinger fetch_person_for_hcard save_person_after_webfinger fetch_private_key fetch_public_key fetch_related_entity queue_public_receive queue_private_receive receive_entity fetch_public_entity fetch_person_url_to update_pod ] # defaults @http_concurrency = 20 @http_timeout = 30 @http_verbose = false @http_redirect_limit = 4 @http_user_agent = "DiasporaFederation/#{DiasporaFederation::VERSION}" class << self # {Callbacks} instance with defined callbacks # @see Callbacks#on # @see Callbacks#trigger # @return [Callbacks] callbacks attr_reader :callbacks # The pod url # # @overload server_uri # @return [URI] the server uri # @overload server_uri= # @example with uri # config.server_uri = URI("http://localhost:3000/") # @example with configured pod_uri # config.server_uri = AppConfig.pod_uri # @param [URI] value the server uri attr_accessor :server_uri # Set the bundle of certificate authorities (CA) certificates # # @overload certificate_authorities # @return [String] path to certificate authorities # @overload certificate_authorities= # @example # config.certificate_authorities = AppConfig.environment.certificate_authorities.get # @param [String] value path to certificate authorities attr_accessor :certificate_authorities # Maximum number of parallel HTTP requests made to other pods (default: +20+) # # @overload http_concurrency # @return [Integer] max number of parallel requests # @overload http_concurrency= # @example # config.http_concurrency = AppConfig.settings.typhoeus_concurrency.to_i # @param [Integer] value max number of parallel requests attr_accessor :http_concurrency # Timeout in seconds for http-requests (default: +30+) # # @overload http_timeout # @return [Integer] http timeout in seconds # @overload http_timeout= # @param [Integer] value http timeout in seconds attr_accessor :http_timeout # Turn on extra verbose output when sending stuff. (default: +false+) # # @overload http_verbose # @return [Boolean] verbose http output # @overload http_verbose= # @example # config.http_verbose = AppConfig.settings.typhoeus_verbose? # @param [Boolean] value verbose http output attr_accessor :http_verbose # Max redirects to follow # @return [Integer] max redirects attr_reader :http_redirect_limit # User agent used for http-requests # @return [String] user agent attr_reader :http_user_agent # Configure the federation library # # @example # DiasporaFederation.configure do |config| # config.server_uri = URI("http://localhost:3000/") # # config.define_callbacks do # # callback configuration # end # end def configure yield self end # Define the callbacks # # In order to communicate with the application which uses the diaspora_federation gem # callbacks are introduced. The callbacks are used for getting required data from the # application or posting data to the application. # # Callbacks are implemented at the application side and must follow these specifications: # # fetch_person_for_webfinger # Fetches person data from the application to form a WebFinger reply # @param [String] diaspora* ID of the person # @return [DiasporaFederation::Discovery::WebFinger] person webfinger data # # fetch_person_for_hcard # Fetches person data from the application to reply for an HCard query # @param [String] guid of the person # @return [DiasporaFederation::Discovery::HCard] person hcard data # # save_person_after_webfinger # After the gem had made a person discovery using WebFinger it calls this callback # so the application saves the person data # @param [DiasporaFederation::Entities::Person] person data # # fetch_private_key # Fetches a private key of a person by her diaspora* ID from the application # @param [String] diaspora* ID of the person # @return [OpenSSL::PKey::RSA] key # # fetch_public_key # Fetches a public key of a person by her diaspora* ID from the application # @param [String] diaspora* ID of the person # @return [OpenSSL::PKey::RSA] key # # fetch_related_entity # Fetches a related entity by a given guid # @param [String] entity_type (Post, Comment, Like, etc) # @param [String] guid of the entity # @return [DiasporaFederation::Entities::RelatedEntity] related entity # # queue_public_receive # Queue a public salmon xml to process in background # @param [String] data salmon slap xml or magic envelope xml # @param [Boolean] legacy true if it is a legacy salmon slap, false if it is a magic envelope xml # # queue_private_receive # Queue a private salmon xml to process in background # @param [String] guid guid of the receiver person # @param [String] data salmon slap xml or encrypted magic envelope json # @param [Boolean] legacy true if it is a legacy salmon slap, false if it is a encrypted magic envelope json # @return [Boolean] true if successful, false if the user was not found # # receive_entity # After the xml was parsed and processed the gem calls this callback to persist the entity # @param [DiasporaFederation::Entity] entity the received entity after processing # @param [Object] recipient_id identifier for the recipient of private messages or nil for public, # see {Receiver.receive_private} # # fetch_public_entity # Fetch a public entity from the database # @param [String] entity_type (Post, StatusMessage, etc) # @param [String] guid the guid of the entity # # fetch_person_url_to # Fetch the url to path for a person # @param [String] diaspora_id # @param [String] path # # update_pod # Update the pod status # @param [String] url the pod url # @param [Symbol, Integer] status the error as {Symbol} or the http-status as {Integer} if it was :ok # # @see Callbacks#on # # @example # config.define_callbacks do # on :some_event do |arg1| # # do something # end # end # # @param [Proc] block the callbacks to define def define_callbacks(&block) @callbacks.instance_eval(&block) end # Validates if the engine is configured correctly # # called from after_initialize # @raise [ConfigurationError] if the configuration is incomplete or invalid def validate_config configuration_error "server_uri: Missing or invalid" unless @server_uri.respond_to? :host unless defined?(::Rails) && !::Rails.env.production? configuration_error "certificate_authorities: Not configured" if @certificate_authorities.nil? unless File.file? @certificate_authorities configuration_error "certificate_authorities: File not found: #{@certificate_authorities}" end end validate_http_config unless @callbacks.definition_complete? configuration_error "Missing handlers for #{@callbacks.missing_handlers.join(', ')}" end logger.info "successfully configured the federation library" end private def validate_http_config configuration_error "http_concurrency: please configure a number" unless @http_concurrency.is_a?(Integer) configuration_error "http_timeout: please configure a number" unless @http_timeout.is_a?(Integer) return unless !@http_verbose.is_a?(TrueClass) && !@http_verbose.is_a?(FalseClass) configuration_error "http_verbose: please configure a boolean" end def configuration_error(message) logger.fatal "diaspora federation configuration error: #{message}" raise ConfigurationError, message end end # Raised, if the engine is not configured correctly class ConfigurationError < RuntimeError end end diaspora_federation-0.2.1/LICENSE0000644000004100000410000010333013146064271016555 0ustar www-datawww-data GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . diaspora_federation-0.2.1/README.md0000644000004100000410000000511113146064271017025 0ustar www-datawww-data# diaspora* federation library ### A library that provides functionalities needed for the diaspora\* federation protocol **master:** [![Build Status master](https://travis-ci.org/diaspora/diaspora_federation.svg?branch=master)](https://travis-ci.org/diaspora/diaspora_federation) | **develop:** [![Build Status develop](https://travis-ci.org/diaspora/diaspora_federation.svg?branch=develop)](https://travis-ci.org/diaspora/diaspora_federation) [![Code Climate](https://codeclimate.com/github/diaspora/diaspora_federation/badges/gpa.svg)](https://codeclimate.com/github/diaspora/diaspora_federation) [![Test Coverage](https://codeclimate.com/github/diaspora/diaspora_federation/badges/coverage.svg)](https://codeclimate.com/github/diaspora/diaspora_federation/coverage) [![Dependency Status](https://gemnasium.com/diaspora/diaspora_federation.svg)](https://gemnasium.com/diaspora/diaspora_federation) [![Inline docs](https://inch-ci.org/github/diaspora/diaspora_federation.svg?branch=master)](https://inch-ci.org/github/diaspora/diaspora_federation) [![Gem Version](https://badge.fury.io/rb/diaspora_federation.svg)](https://badge.fury.io/rb/diaspora_federation) [Gem Documentation](http://www.rubydoc.info/gems/diaspora_federation/) | [Protocol Documentation](https://diaspora.github.io/diaspora_federation/) | [Bugtracker](https://github.com/diaspora/diaspora_federation/issues) This repository contains two gems: * `diaspora_federation` provides the functionality for de-/serialization and de-/encryption of Entities in the protocols used for communication among the various installations of diaspora\*. * `diaspora_federation-rails` is a rails engine that adds the diaspora\* federation protocol to a rails app. ## Usage Add the gem to your ```Gemfile```: ```ruby gem "diaspora_federation-rails" ``` Mount the routes in your ```config/routes.rb```: ```ruby mount DiasporaFederation::Engine => "/" ``` Configure the engine in ```config/initializers/diaspora_federation.rb```: ```ruby DiasporaFederation.configure do |config| # the pod url config.server_uri = AppConfig.pod_uri config.define_callbacks do on :fetch_person_for_webfinger do |diaspora_id| person = Person.find_local_by_diaspora_id(diaspora_id) if person DiasporaFederation::Discovery::WebFinger.new( # ... ) end end on :fetch_person_for_hcard do |guid| # ... end end end ``` ## Contributing See [our contribution guide](/CONTRIBUTING.md) for more information on how to contribute to the diaspora\* federation library. ## License [GNU Affero General Public License](/LICENSE).