diff --git a/lib/diaspora_federation/federation.rb b/lib/diaspora_federation/federation.rb index b8732fc..a55eed4 100644 --- a/lib/diaspora_federation/federation.rb +++ b/lib/diaspora_federation/federation.rb @@ -4,5 +4,6 @@ module DiasporaFederation end end +require "diaspora_federation/federation/fetcher" require "diaspora_federation/federation/receiver" require "diaspora_federation/federation/sender" diff --git a/lib/diaspora_federation/federation/fetcher.rb b/lib/diaspora_federation/federation/fetcher.rb new file mode 100644 index 0000000..2deb9b0 --- /dev/null +++ b/lib/diaspora_federation/federation/fetcher.rb @@ -0,0 +1,26 @@ +module 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_type}/#{guid}") + response = DiasporaFederation::Fetcher.get(url) + raise "Failed to fetch #{url}: #{response.status}" unless response.success? + + magic_env = Nokogiri::XML::Document.parse(response.body).root + entity = Salmon::MagicEnvelope.unenvelop(magic_env) + DiasporaFederation.callbacks.trigger(:save_entity_after_receive, entity) + rescue => e + raise NotFetchable, "Failed to fetch #{entity_type}:#{guid} from #{author}: #{e.class}: #{e.message}" + end + + # Raised, if the entity is not fetchable + class NotFetchable < RuntimeError + end + end + end +end diff --git a/spec/lib/diaspora_federation/federation/fetcher_spec.rb b/spec/lib/diaspora_federation/federation/fetcher_spec.rb new file mode 100644 index 0000000..2972a46 --- /dev/null +++ b/spec/lib/diaspora_federation/federation/fetcher_spec.rb @@ -0,0 +1,76 @@ +module DiasporaFederation + describe Federation::Fetcher do + let(:post) { FactoryGirl.build(:status_message_entity, public: true) } + let(:post_magic_env) { Salmon::MagicEnvelope.new(post).envelop(alice.private_key, post.author).to_xml } + + describe ".fetch_public" do + it "fetches a public post" do + stub_request(:get, "https://example.org/fetch/post/#{post.guid}") + .to_return(status: 200, body: post_magic_env) + + expect(DiasporaFederation.callbacks).to receive(:trigger).with( + :fetch_person_url_to, post.author, "/fetch/post/#{post.guid}" + ).and_return("https://example.org/fetch/post/#{post.guid}") + expect(DiasporaFederation.callbacks).to receive(:trigger).with( + :fetch_public_key_by_diaspora_id, post.author + ).and_return(alice.public_key) + expect(DiasporaFederation.callbacks).to receive(:trigger).with( + :save_entity_after_receive, kind_of(Entities::StatusMessage) + ) do |_, entity| + expect(entity.guid).to eq(post.guid) + expect(entity.author).to eq(post.author) + expect(entity.raw_message).to eq(post.raw_message) + expect(entity.public).to eq("true") + end + + Federation::Fetcher.fetch_public(post.author, :post, post.guid) + end + + it "follows redirects" do + stub_request(:get, "https://example.org/fetch/post/#{post.guid}") + .to_return(status: 302, headers: {"Location" => "https://example.com/fetch/post/#{post.guid}"}) + stub_request(:get, "https://example.com/fetch/post/#{post.guid}") + .to_return(status: 200, body: post_magic_env) + + expect(DiasporaFederation.callbacks).to receive(:trigger).with( + :fetch_person_url_to, post.author, "/fetch/post/#{post.guid}" + ).and_return("https://example.org/fetch/post/#{post.guid}") + expect(DiasporaFederation.callbacks).to receive(:trigger).with( + :fetch_public_key_by_diaspora_id, post.author + ).and_return(alice.public_key) + expect(DiasporaFederation.callbacks).to receive(:trigger).with( + :save_entity_after_receive, kind_of(Entities::StatusMessage) + ) + + Federation::Fetcher.fetch_public(post.author, :post, post.guid) + end + + it "raises NotFetchable if post not found (private)" do + stub_request(:get, "https://example.org/fetch/post/#{post.guid}") + .to_return(status: 404) + + expect(DiasporaFederation.callbacks).to receive(:trigger).with( + :fetch_person_url_to, post.author, "/fetch/post/#{post.guid}" + ).and_return("https://example.org/fetch/post/#{post.guid}") + + expect { + Federation::Fetcher.fetch_public(post.author, :post, post.guid) + }.to raise_error Federation::Fetcher::NotFetchable + end + + it "raises NotFetchable if connection refused" do + expect(DiasporaFederation::Fetcher).to receive(:get).with( + "https://example.org/fetch/post/#{post.guid}" + ).and_raise(Faraday::ConnectionFailed, "Couldn't connect to server") + + expect(DiasporaFederation.callbacks).to receive(:trigger).with( + :fetch_person_url_to, post.author, "/fetch/post/#{post.guid}" + ).and_return("https://example.org/fetch/post/#{post.guid}") + + expect { + Federation::Fetcher.fetch_public(post.author, :post, post.guid) + }.to raise_error Federation::Fetcher::NotFetchable + end + end + end +end diff --git a/spec/support/shared_entity_specs.rb b/spec/support/shared_entity_specs.rb index e0e747f..8890a20 100644 --- a/spec/support/shared_entity_specs.rb +++ b/spec/support/shared_entity_specs.rb @@ -85,10 +85,8 @@ shared_examples "a relayable Entity" do author_signature = xml.at_xpath("post/*[1]/author_signature").text parent_author_signature = xml.at_xpath("post/*[1]/parent_author_signature").text - alice_public_key = OpenSSL::PKey::RSA.new(alice.serialized_public_key) - bob_public_key = OpenSSL::PKey::RSA.new(bob.serialized_public_key) - expect(verify_signature(alice_public_key, author_signature, signed_string)).to be_truthy - expect(verify_signature(bob_public_key, parent_author_signature, signed_string)).to be_truthy + expect(verify_signature(alice.public_key, author_signature, signed_string)).to be_truthy + expect(verify_signature(bob.public_key, parent_author_signature, signed_string)).to be_truthy end end end diff --git a/test/dummy/app/models/person.rb b/test/dummy/app/models/person.rb index 013979c..00709a6 100644 --- a/test/dummy/app/models/person.rb +++ b/test/dummy/app/models/person.rb @@ -1,6 +1,9 @@ class Person < ActiveRecord::Base include ::Diaspora::Guid + def private_key; OpenSSL::PKey::RSA.new(serialized_private_key) end + def public_key; OpenSSL::PKey::RSA.new(serialized_public_key) end + def alias_url; "#{url}people/#{guid}" end def hcard_url; "#{url}hcard/users/#{guid}" end def profile_url; "#{url}u/#{nickname}" end