From 6b0507949c623a4cd3e4f96dccdc337680cf3d81 Mon Sep 17 00:00:00 2001 From: Maxwell Salzberg Date: Thu, 8 Sep 2011 16:19:50 -0700 Subject: [PATCH 1/8] updated salmon to let us not always encrypt the header --- app/controllers/publics_controller.rb | 23 +++-- app/models/post.rb | 2 +- config/routes.rb | 1 + db/schema.rb | 2 +- lib/salmon/salmon.rb | 48 ++++++++--- spec/controllers/publics_controller_spec.rb | 18 ++++ spec/lib/salmon_salmon_spec.rb | 94 --------------------- 7 files changed, 73 insertions(+), 115 deletions(-) delete mode 100644 spec/lib/salmon_salmon_spec.rb diff --git a/app/controllers/publics_controller.rb b/app/controllers/publics_controller.rb index 3a9cc2290..3b555bd1e 100644 --- a/app/controllers/publics_controller.rb +++ b/app/controllers/publics_controller.rb @@ -4,12 +4,14 @@ class PublicsController < ApplicationController require File.join(Rails.root, '/lib/diaspora/parser') + require File.join(Rails.root, '/lib/postzord/receiver/public') include Diaspora::Parser skip_before_filter :set_header_data skip_before_filter :which_action_and_user skip_before_filter :set_grammatical_gender before_filter :allow_cross_origin, :only => [:hcard, :host_meta, :webfinger] + before_filter :check_for_xml, :only => [:receive, :receive_public] respond_to :html respond_to :xml, :only => :post @@ -47,12 +49,12 @@ class PublicsController < ApplicationController render :text => params['hub.challenge'], :status => 202, :layout => false end - def receive - if params[:xml].nil? - render :nothing => true, :status => 422 - return - end + def receive_public + Postzord::Receiver::Public.new(params[:xml]) + render :nothing => true, :status => :ok + end + def receive person = Person.where(:guid => params[:guid]).first if person.nil? || person.owner_id.nil? @@ -66,4 +68,15 @@ class PublicsController < ApplicationController render :nothing => true, :status => 202 end + + + private + + def check_for_xml + if params[:xml].nil? + render :nothing => true, :status => 422 + return + end + end + end diff --git a/app/models/post.rb b/app/models/post.rb index 6a04e685f..a686d31a4 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -102,7 +102,7 @@ class Post < ActiveRecord::Base return local_post end elsif !local_post - if self.save + if self.save user.contact_for(person).receive_post(self) user.notify_if_mentioned(self) Rails.logger.info("event=receive payload_type=#{self.class} update=false status=complete sender=#{self.diaspora_handle}") diff --git a/config/routes.rb b/config/routes.rb index bbdec44f0..3b3a98371 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -122,6 +122,7 @@ Diaspora::Application.routes.draw do get 'hcard/users/:guid' => :hcard get '.well-known/host-meta' => :host_meta post 'receive/users/:guid' => :receive + post 'receive/public' => :receive_public get 'hub' => :hub end diff --git a/db/schema.rb b/db/schema.rb index 96d8cda46..ea38c2f40 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -83,7 +83,7 @@ ActiveRecord::Schema.define(:version => 20110911213207) do t.datetime "updated_at" end - add_index "conversation_visibilities", ["conversation_id", "person_id"], :name => "index_conversation_visibilities_usefully", :unique => true + add_index "conversation_visibilities", ["conversation_id", "person_id"], :name => "index_conversation_visibilities_on_conversation_id_and_person_id", :unique => true add_index "conversation_visibilities", ["conversation_id"], :name => "index_conversation_visibilities_on_conversation_id" add_index "conversation_visibilities", ["person_id"], :name => "index_conversation_visibilities_on_person_id" diff --git a/lib/salmon/salmon.rb b/lib/salmon/salmon.rb index 331322024..2164ba8b9 100644 --- a/lib/salmon/salmon.rb +++ b/lib/salmon/salmon.rb @@ -54,37 +54,47 @@ module Salmon salmon end - def self.parse(xml, user) + def self.parse(xml, user=nil) slap = self.new doc = Nokogiri::XML(xml) sig_doc = doc.search('entry') ### Header ## - decrypted_header = user.decrypt(doc.search('encrypted_header').text) - header_doc = Nokogiri::XML(decrypted_header) + header_doc = slap.salmon_header(doc, user) slap.author_email= header_doc.search('uri').text.split("acct:").last slap.aes_key = header_doc.search('aes_key').text slap.iv = header_doc.search('iv').text slap.magic_sig = MagicSigEnvelope.parse sig_doc - if 'base64url' == slap.magic_sig.encoding - - key_hash = {'key' => slap.aes_key, 'iv' => slap.iv} - slap.parsed_data = user.aes_decrypt(decode64url(slap.magic_sig.data), key_hash) - slap.sig = slap.magic_sig.sig - else - raise ArgumentError, "Magic Signature data must be encoded with base64url, was #{slap.magic_sig.encoding}" - end - + key_hash = {'key' => slap.aes_key, 'iv' => slap.iv} + slap.parsed_data = slap.parse_data(key_hash, user) + slap.sig = slap.magic_sig.sig slap.data_type = slap.magic_sig.data_type - raise ArgumentError, "Magic Signature data must be signed with RSA-SHA256, was #{slap.magic_sig.alg}" unless 'RSA-SHA256' == slap.magic_sig.alg - slap end + def parse_data(key_hash, user) + data = SalmonSlap.decode64url(self.magic_sig.data) + if user.present? + user.aes_decrypt(data, key_hash) + else + data + end + end + + # @return [Nokogiri::Doc] + def salmon_header(doc, user) + if user.present? + decrypted_header = user.decrypt(doc.search('encrypted_header').text) + Nokogiri::XML(decrypted_header) + else + doc.search('header') + end + end + def xml_for person xml =< @@ -190,10 +200,20 @@ HEADER env = self.new ns = {'me'=>'http://salmon-protocol.org/ns/magic-env'} env.encoding = doc.search('//me:env/me:encoding', ns).text.strip + + if env.encoding != 'base64url' + raise ArgumentError, "Magic Signature data must be encoded with base64url, was #{slap.magic_sig.encoding}" + end + env.data = doc.search('//me:env/me:data', ns).text env.alg = doc.search('//me:env/me:alg', ns).text.strip env.sig = doc.search('//me:env/me:sig', ns).text env.data_type = doc.search('//me:env/me:data', ns).first['type'].strip + + unless 'RSA-SHA256' == env.alg + raise ArgumentError, "Magic Signature data must be signed with RSA-SHA256, was #{env.alg}" + end + env end diff --git a/spec/controllers/publics_controller_spec.rb b/spec/controllers/publics_controller_spec.rb index d50b42ac6..16192a4a9 100644 --- a/spec/controllers/publics_controller_spec.rb +++ b/spec/controllers/publics_controller_spec.rb @@ -20,6 +20,24 @@ describe PublicsController do end end + describe '#receive_public' do + it 'succeeds' do + post :receive_public, :xml => "" + response.should be_success + end + + it 'returns a 422 if no xml is passed' do + post :receive_public + response.code.should == '422' + end + + it 'calls Postzord::Receiver:Public' do + xml = "stuff" + Postzord::Receiver::Public.should_receive(:new).with(xml) + post :receive_public, :xml => xml + end + end + describe '#receive' do let(:xml) { "" } diff --git a/spec/lib/salmon_salmon_spec.rb b/spec/lib/salmon_salmon_spec.rb deleted file mode 100644 index 54bba6d71..000000000 --- a/spec/lib/salmon_salmon_spec.rb +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (c) 2010, Diaspora Inc. This file is -# licensed under the Affero General Public License version 3 or later. See -# the COPYRIGHT file. - -require 'spec_helper' - -describe Salmon do - let(:user){alice} - let(:user2) {eve} - let(:user3) {Factory.create(:user)} - let(:post){ user.post :status_message, :text => "hi", :to => user.aspects.create(:name => "sdg").id } - - let!(:created_salmon) {Salmon::SalmonSlap.create(user, post.to_diaspora_xml)} - - describe '#create' do - - it 'has data in the magic envelope' do - created_salmon.magic_sig.data.should_not be nil - end - - it 'has no parsed_data' do - created_salmon.parsed_data.should be nil - end - - it 'sets aes and iv key' do - created_salmon.aes_key.should_not be nil - created_salmon.iv.should_not be nil - end - - it 'makes the data in the signature encrypted with that key' do - key_hash = {'key' => created_salmon.aes_key, 'iv' => created_salmon.iv} - decoded_string = Salmon::SalmonSlap.decode64url(created_salmon.magic_sig.data) - user.aes_decrypt(decoded_string, key_hash).should == post.to_diaspora_xml - end - end - - describe '#xml_for' do - let(:xml) {created_salmon.xml_for user2.person} - - it 'has a encrypted header field' do - xml.include?("encrypted_header").should be true - end - - it 'the encrypted_header field should contain the aes key' do - doc = Nokogiri::XML(xml) - decrypted_header = user2.decrypt(doc.search('encrypted_header').text) - decrypted_header.include?(created_salmon.aes_key).should be true - end - end - - context 'marshaling' do - let(:xml) {created_salmon.xml_for user2.person} - let(:parsed_salmon) { Salmon::SalmonSlap.parse(xml, user2)} - - it 'should parse out the aes key' do - parsed_salmon.aes_key.should == created_salmon.aes_key - end - - it 'should parse out the iv' do - parsed_salmon.iv.should == created_salmon.iv - end - it 'should parse out the authors diaspora_handle' do - parsed_salmon.author_email.should == user.person.diaspora_handle - - end - - describe '#author' do - it 'should reference a local author' do - parsed_salmon.author.should == user.person - end - - it 'should fail if no author is found' do - parsed_salmon.author_email = 'tom@tom.joindiaspora.com' - - - proc {parsed_salmon.author.public_key}.should raise_error "did you remember to async webfinger?" - - end - - end - - it 'verifies the signature for the sender' do - parsed_salmon.verified_for_key?(user.public_key).should be true - end - - it 'contains the original data' do - parsed_salmon.parsed_data.should == post.to_diaspora_xml - end - - end - - - -end From e0429ee823f7f5c948758cb6426bb265bb84772a Mon Sep 17 00:00:00 2001 From: Maxwell Salzberg Date: Thu, 8 Sep 2011 17:47:48 -0700 Subject: [PATCH 2/8] MS DG pulling apart salmon and making our custom hacks more obvious --- app/models/job/http_multi.rb | 2 +- app/models/user.rb | 2 +- lib/postzord/dispatch.rb | 2 +- lib/postzord/receiver.rb | 2 +- lib/postzord/receiver/public.rb | 16 ++ lib/salmon/encrypted_salmon_slap.rb | 25 ++ lib/salmon/magic_sig_envelope.rb | 70 ++++++ lib/salmon/salmon.rb | 224 +----------------- lib/salmon/salmon_slap.rb | 150 ++++++++++++ spec/lib/postzord/receiver/public.rb | 32 +++ spec/lib/postzord/receiver/public_spec.rb | 59 +++++ spec/lib/salmon/encrypted_salmon_slap_spec.rb | 89 +++++++ spec/lib/salmon/magic_sig_envelope_spec.rb | 5 + spec/lib/salmon/salmon_slap_spec.rb | 5 + 14 files changed, 458 insertions(+), 225 deletions(-) create mode 100644 lib/postzord/receiver/public.rb create mode 100644 lib/salmon/encrypted_salmon_slap.rb create mode 100644 lib/salmon/magic_sig_envelope.rb create mode 100644 lib/salmon/salmon_slap.rb create mode 100644 spec/lib/postzord/receiver/public.rb create mode 100644 spec/lib/postzord/receiver/public_spec.rb create mode 100644 spec/lib/salmon/encrypted_salmon_slap_spec.rb create mode 100644 spec/lib/salmon/magic_sig_envelope_spec.rb create mode 100644 spec/lib/salmon/salmon_slap_spec.rb diff --git a/app/models/job/http_multi.rb b/app/models/job/http_multi.rb index d24838717..40068c966 100644 --- a/app/models/job/http_multi.rb +++ b/app/models/job/http_multi.rb @@ -19,7 +19,7 @@ module Job people = Person.where(:id => person_ids) - salmon = Salmon::SalmonSlap.create(user, Base64.decode64(enc_object_xml)) + salmon = Salmon::EncryptedSalmonSlap.create(user, Base64.decode64(enc_object_xml)) failed_request_people = [] diff --git a/app/models/user.rb b/app/models/user.rb index 10fef5322..31731d743 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -215,7 +215,7 @@ class User < ActiveRecord::Base end def salmon(post) - Salmon::SalmonSlap.create(self, post.to_diaspora_xml) + Salmon::EncryptedSalmonSlap.create(self, post.to_diaspora_xml) end def build_relayable(model, options = {}) diff --git a/lib/postzord/dispatch.rb b/lib/postzord/dispatch.rb index c3390a75b..81c61e35c 100644 --- a/lib/postzord/dispatch.rb +++ b/lib/postzord/dispatch.rb @@ -18,7 +18,7 @@ class Postzord::Dispatch end def salmon - @salmon_factory ||= Salmon::SalmonSlap.create(@sender, @xml) + @salmon_factory ||= Salmon::EncryptedSalmonSlap.create(@sender, @xml) end def post(opts = {}) diff --git a/lib/postzord/receiver.rb b/lib/postzord/receiver.rb index 37029a5f8..4a49647ad 100644 --- a/lib/postzord/receiver.rb +++ b/lib/postzord/receiver.rb @@ -45,7 +45,7 @@ module Postzord protected def salmon - @salmon ||= Salmon::SalmonSlap.parse(@salmon_xml, @user) + @salmon ||= Salmon::EncryptedSalmonSlap.parse(@salmon_xml, @user) end def xml_author diff --git a/lib/postzord/receiver/public.rb b/lib/postzord/receiver/public.rb new file mode 100644 index 000000000..c2b6199ec --- /dev/null +++ b/lib/postzord/receiver/public.rb @@ -0,0 +1,16 @@ +# Copyright (c) 2010, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. +# +module Postzord + class Receiver + class Public + attr_accessor :xml + + def initialize(xml) + @xml = xml + + end + end + end +end diff --git a/lib/salmon/encrypted_salmon_slap.rb b/lib/salmon/encrypted_salmon_slap.rb new file mode 100644 index 000000000..d40a5c390 --- /dev/null +++ b/lib/salmon/encrypted_salmon_slap.rb @@ -0,0 +1,25 @@ +# Copyright (c) 2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +module Salmon + class EncryptedSalmonSlap < SalmonSlap + def header(person) + < + #{person.encrypt("#{plaintext_header}")} + +XML + end + + def parse_data(key_hash, user) + user.aes_decrypt(super, key_hash) + end + + # @return [Nokogiri::Doc] + def salmon_header(doc, user) + header = user.decrypt(doc.search('encrypted_header').text) + Nokogiri::XML(header) + end + end +end diff --git a/lib/salmon/magic_sig_envelope.rb b/lib/salmon/magic_sig_envelope.rb new file mode 100644 index 000000000..cd7b9c34f --- /dev/null +++ b/lib/salmon/magic_sig_envelope.rb @@ -0,0 +1,70 @@ +# Copyright (c) 2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +module Salmon + class MagicSigEnvelope + attr_accessor :data, :data_type, :encoding, :alg, :sig, :author + def self.parse(doc) + env = self.new + ns = {'me'=>'http://salmon-protocol.org/ns/magic-env'} + env.encoding = doc.search('//me:env/me:encoding', ns).text.strip + + if env.encoding != 'base64url' + raise ArgumentError, "Magic Signature data must be encoded with base64url, was #{slap.magic_sig.encoding}" + end + + env.data = doc.search('//me:env/me:data', ns).text + env.alg = doc.search('//me:env/me:alg', ns).text.strip + env.sig = doc.search('//me:env/me:sig', ns).text + env.data_type = doc.search('//me:env/me:data', ns).first['type'].strip + + unless 'RSA-SHA256' == env.alg + raise ArgumentError, "Magic Signature data must be signed with RSA-SHA256, was #{env.alg}" + end + + env + end + + def self.create(user, activity) + env = MagicSigEnvelope.new + env.author = user.person + env.data = Base64.urlsafe_encode64(activity) + env.data_type = env.get_data_type + env.encoding = env.get_encoding + env.alg = env.get_alg + + env.sig = Base64.urlsafe_encode64( + user.encryption_key.sign OpenSSL::Digest::SHA256.new, env.signable_string ) + + env + end + + def signable_string + [@data, Base64.urlsafe_encode64(@data_type),Base64.urlsafe_encode64(@encoding), Base64.urlsafe_encode64(@alg)].join(".") + end + + def to_xml + < + #{@data} + #{@encoding} + #{@alg} + #{@sig} + +ENTRY + end + + def get_encoding + 'base64url' + end + + def get_data_type + 'application/atom+xml' + end + + def get_alg + 'RSA-SHA256' + end + end +end diff --git a/lib/salmon/salmon.rb b/lib/salmon/salmon.rb index 2164ba8b9..0099fe0b4 100644 --- a/lib/salmon/salmon.rb +++ b/lib/salmon/salmon.rb @@ -39,225 +39,7 @@ end # Verify documents secured with Magic Signatures module Salmon - - class SalmonSlap - attr_accessor :magic_sig, :author, :author_email, :aes_key, :iv, :parsed_data, - :data_type, :sig - - def self.create(user, activity) - salmon = self.new - salmon.author = user.person - aes_key_hash = user.person.gen_aes_key - salmon.aes_key = aes_key_hash['key'] - salmon.iv = aes_key_hash['iv'] - salmon.magic_sig = MagicSigEnvelope.create(user , user.person.aes_encrypt(activity, aes_key_hash)) - salmon - end - - def self.parse(xml, user=nil) - slap = self.new - doc = Nokogiri::XML(xml) - - sig_doc = doc.search('entry') - - ### Header ## - header_doc = slap.salmon_header(doc, user) - slap.author_email= header_doc.search('uri').text.split("acct:").last - slap.aes_key = header_doc.search('aes_key').text - slap.iv = header_doc.search('iv').text - - slap.magic_sig = MagicSigEnvelope.parse sig_doc - - key_hash = {'key' => slap.aes_key, 'iv' => slap.iv} - slap.parsed_data = slap.parse_data(key_hash, user) - slap.sig = slap.magic_sig.sig - slap.data_type = slap.magic_sig.data_type - - slap - end - - def parse_data(key_hash, user) - data = SalmonSlap.decode64url(self.magic_sig.data) - if user.present? - user.aes_decrypt(data, key_hash) - else - data - end - end - - # @return [Nokogiri::Doc] - def salmon_header(doc, user) - if user.present? - decrypted_header = user.decrypt(doc.search('encrypted_header').text) - Nokogiri::XML(decrypted_header) - else - doc.search('header') - end - end - - def xml_for person - xml =< - - #{person.encrypt(decrypted_header)} - #{@magic_sig.to_xml} - -ENTRY - - end - - def decrypted_header - header =<
- #{iv} - #{aes_key} - - #{@author.name} - acct:#{@author.diaspora_handle} - - -HEADER - end - - def author - if @author.nil? - @author ||= Person.by_account_identifier @author_email - raise "did you remember to async webfinger?" if @author.nil? - end - @author - end - - # Decode URL-safe-Base64. This implements - def self.decode64url(str) - # remove whitespace - sans_whitespace = str.gsub(/\s/, '') - # pad to a multiple of 4 - string = sans_whitespace + '=' * ((4 - sans_whitespace.size) % 4) - # convert to standard Base64 - # string = padded.tr('-','+').tr('_','/') - - # Base64.decode64(string) - Base64.urlsafe_decode64 string - end - - # Check whether this envelope's signature can be verified with the - # provided OpenSSL::PKey::RSA public_key. - # Example: - # - # env.verified_for_key? OpenSSL::PKey::RSA.new(File.open('public_key.pem')) - # # -> true - def verified_for_key?(public_key) - signature = Base64.urlsafe_decode64(self.magic_sig.sig) - signed_data = self.magic_sig.signable_string# Base64.urlsafe_decode64(self.magic_sig.signable_string) - - public_key.verify(OpenSSL::Digest::SHA256.new, signature, signed_data ) - end - - # Decode a string containing URL safe Base64 into an integer - # Example: - # - # MagicSig.b64_to_n('AQAB') - # # -> 645537 - def self.b64_to_n(str) - packed = decode64url(str) - packed.unpack('B*')[0].to_i(2) - end - - # Parse a string containing a magic-public-key into an OpenSSL::PKey::RSA key. - # Example: - # - # key = MagicSig.parse_key('RSA.mVgY8RN6URBTstndvmUUPb4UZTdwvwmddSKE5z_jvKUEK6yk1u3rrC9yN8k6FilGj9K0eeUPe2hf4Pj-5CmHww.AQAB') - # key.n - # # -> 8031283789075196565022891546563591368344944062154100509645398892293433370859891943306439907454883747534493461257620351548796452092307094036643522661681091 - # key.e - # # -> 65537 - def self.parse_key(str) - n,e = str.match(/^RSA.([^.]*).([^.]*)$/)[1..2] - build_key(b64_to_n(n),b64_to_n(e)) - end - - # Take two integers e, n and create a new OpenSSL::PKey::RSA key with them - # Example: - # - # n = 9487834027867356975347184933768917275269369900665861930617802608089634337052392076689226301419587057117740995382286148368168197915234368486155306558161867 - # e = 65537 - # key = MagicSig.build_key(n,e) - # key.public_encrypt(...) # for sending to strangers - # key.public_decrypt(...) # very rarely used - # key.verify(...) # for verifying signatures - def self.build_key(n,e) - key = OpenSSL::PKey::RSA.new - key.n = n - key.e = e - key - end - - end - - class MagicSigEnvelope - attr_accessor :data, :data_type, :encoding, :alg, :sig, :author - def self.parse(doc) - env = self.new - ns = {'me'=>'http://salmon-protocol.org/ns/magic-env'} - env.encoding = doc.search('//me:env/me:encoding', ns).text.strip - - if env.encoding != 'base64url' - raise ArgumentError, "Magic Signature data must be encoded with base64url, was #{slap.magic_sig.encoding}" - end - - env.data = doc.search('//me:env/me:data', ns).text - env.alg = doc.search('//me:env/me:alg', ns).text.strip - env.sig = doc.search('//me:env/me:sig', ns).text - env.data_type = doc.search('//me:env/me:data', ns).first['type'].strip - - unless 'RSA-SHA256' == env.alg - raise ArgumentError, "Magic Signature data must be signed with RSA-SHA256, was #{env.alg}" - end - - env - end - - def self.create(user, activity) - env = MagicSigEnvelope.new - env.author = user.person - env.data = Base64.urlsafe_encode64(activity) - env.data_type = env.get_data_type - env.encoding = env.get_encoding - env.alg = env.get_alg - - env.sig = Base64.urlsafe_encode64( - user.encryption_key.sign OpenSSL::Digest::SHA256.new, env.signable_string ) - - env - end - - def signable_string - [@data, Base64.urlsafe_encode64(@data_type),Base64.urlsafe_encode64(@encoding), Base64.urlsafe_encode64(@alg)].join(".") - end - - def to_xml - xml= < - #{@data} - #{@encoding} - #{@alg} - #{@sig} - -ENTRY - xml - end - - def get_encoding - 'base64url' - end - - def get_data_type - 'application/atom+xml' - end - - def get_alg - 'RSA-SHA256' - end - - end + autoload :SalmonSlap, File.join(Rails.root, "lib", "salmon", "salmon_slap") + autoload :EncryptedSalmonSlap, File.join(Rails.root, "lib", "salmon", "encrypted_salmon_slap") + autoload :MagicSigEnvelope, File.join(Rails.root, "lib", "salmon", "magic_sig_envelope") end diff --git a/lib/salmon/salmon_slap.rb b/lib/salmon/salmon_slap.rb new file mode 100644 index 000000000..3a7201662 --- /dev/null +++ b/lib/salmon/salmon_slap.rb @@ -0,0 +1,150 @@ +# Copyright (c) 2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +module Salmon + class SalmonSlap + attr_accessor :magic_sig, :author, :author_email, :aes_key, :iv, :parsed_data, + :data_type, :sig + + def self.create(user, activity) + salmon = self.new + salmon.author = user.person + aes_key_hash = user.person.gen_aes_key + salmon.aes_key = aes_key_hash['key'] + salmon.iv = aes_key_hash['iv'] + salmon.magic_sig = MagicSigEnvelope.create(user , user.person.aes_encrypt(activity, aes_key_hash)) + salmon + end + + def self.parse(xml, user=nil) + slap = self.new + doc = Nokogiri::XML(xml) + + sig_doc = doc.search('entry') + + ### Header ## + header_doc = slap.salmon_header(doc, user) + slap.author_email= header_doc.search('uri').text.split("acct:").last + slap.aes_key = header_doc.search('aes_key').text + slap.iv = header_doc.search('iv').text + + slap.magic_sig = MagicSigEnvelope.parse sig_doc + + key_hash = {'key' => slap.aes_key, 'iv' => slap.iv} + slap.parsed_data = slap.parse_data(key_hash, user) + slap.sig = slap.magic_sig.sig + slap.data_type = slap.magic_sig.data_type + + slap + end + + # @return [String] + def parse_data(key_hash, user=nil) + SalmonSlap.decode64url(self.magic_sig.data) + end + + # @return [Nokogiri::Doc] + def salmon_header(doc, user=nil) + doc.search('header') + end + + def xml_for(person) + xml =< + + #{header(person)} + #{@magic_sig.to_xml} + +ENTRY + end + + def header(person) + "
#{plaintext_header}
" + end + + def plaintext_header + header =<
#{iv} + #{aes_key} + + #{@author.name} + acct:#{@author.diaspora_handle} + +HEADER + end + + def author + if @author.nil? + @author ||= Person.by_account_identifier @author_email + raise "did you remember to async webfinger?" if @author.nil? + end + @author + end + + # Decode URL-safe-Base64. This implements + def self.decode64url(str) + # remove whitespace + sans_whitespace = str.gsub(/\s/, '') + # pad to a multiple of 4 + string = sans_whitespace + '=' * ((4 - sans_whitespace.size) % 4) + # convert to standard Base64 + # string = padded.tr('-','+').tr('_','/') + + # Base64.decode64(string) + Base64.urlsafe_decode64 string + end + + # Check whether this envelope's signature can be verified with the + # provided OpenSSL::PKey::RSA public_key. + # Example: + # + # env.verified_for_key? OpenSSL::PKey::RSA.new(File.open('public_key.pem')) + # # -> true + def verified_for_key?(public_key) + signature = Base64.urlsafe_decode64(self.magic_sig.sig) + signed_data = self.magic_sig.signable_string# Base64.urlsafe_decode64(self.magic_sig.signable_string) + + public_key.verify(OpenSSL::Digest::SHA256.new, signature, signed_data ) + end + + # Decode a string containing URL safe Base64 into an integer + # Example: + # + # MagicSig.b64_to_n('AQAB') + # # -> 645537 + def self.b64_to_n(str) + packed = decode64url(str) + packed.unpack('B*')[0].to_i(2) + end + + # Parse a string containing a magic-public-key into an OpenSSL::PKey::RSA key. + # Example: + # + # key = MagicSig.parse_key('RSA.mVgY8RN6URBTstndvmUUPb4UZTdwvwmddSKE5z_jvKUEK6yk1u3rrC9yN8k6FilGj9K0eeUPe2hf4Pj-5CmHww.AQAB') + # key.n + # # -> 8031283789075196565022891546563591368344944062154100509645398892293433370859891943306439907454883747534493461257620351548796452092307094036643522661681091 + # key.e + # # -> 65537 + def self.parse_key(str) + n,e = str.match(/^RSA.([^.]*).([^.]*)$/)[1..2] + build_key(b64_to_n(n),b64_to_n(e)) + end + + # Take two integers e, n and create a new OpenSSL::PKey::RSA key with them + # Example: + # + # n = 9487834027867356975347184933768917275269369900665861930617802608089634337052392076689226301419587057117740995382286148368168197915234368486155306558161867 + # e = 65537 + # key = MagicSig.build_key(n,e) + # key.public_encrypt(...) # for sending to strangers + # key.public_decrypt(...) # very rarely used + # key.verify(...) # for verifying signatures + def self.build_key(n,e) + key = OpenSSL::PKey::RSA.new + key.n = n + key.e = e + key + end + end +end diff --git a/spec/lib/postzord/receiver/public.rb b/spec/lib/postzord/receiver/public.rb new file mode 100644 index 000000000..5b9259d09 --- /dev/null +++ b/spec/lib/postzord/receiver/public.rb @@ -0,0 +1,32 @@ +# Copyright (c) 2010, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +require 'spec_helper' + +require File.join(Rails.root, 'lib/postzord') +require File.join(Rails.root, 'lib/postzord/receiver/public') + +describe Postzord::Receiver::Public do + + describe '#initialize' do + + end + + describe '#verify_signature' do + + end + + describe '#collect_recipients' do + + end + + describe '#batch_insert_visibilities' do + + end + + describe '#batch_notify' do + + end + +end diff --git a/spec/lib/postzord/receiver/public_spec.rb b/spec/lib/postzord/receiver/public_spec.rb new file mode 100644 index 000000000..318cc1a02 --- /dev/null +++ b/spec/lib/postzord/receiver/public_spec.rb @@ -0,0 +1,59 @@ +# Copyright (c) 2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +require 'spec_helper' + +require File.join(Rails.root, 'lib/postzord') +require File.join(Rails.root, 'lib/postzord/receiver/public') + +describe Postzord::Receiver::Public do + + describe '#initialize' do + it 'sets xml as instance variable' do + receiver = Postzord::Receiver::Public.new("blah") + receiver.xml.should == 'blah' + end + end + + describe '#perform' do + it 'calls verify_signature' do + + end + + context 'if signature is valid' do + it 'calls collect_recipients' do + + end + + it 'saves the parsed object' do + + end + + it 'calls batch_insert_visibilities' do + + end + + it 'calls batch_notify' do + + end + end + end + + describe '#verify_signature' do + + end + + describe '#collect_recipients' do + + end + + describe '#batch_insert_visibilities' do + + end + + describe '#batch_notify' do + + end + +end diff --git a/spec/lib/salmon/encrypted_salmon_slap_spec.rb b/spec/lib/salmon/encrypted_salmon_slap_spec.rb new file mode 100644 index 000000000..03b5dd2fb --- /dev/null +++ b/spec/lib/salmon/encrypted_salmon_slap_spec.rb @@ -0,0 +1,89 @@ +# Copyright (c) 2010, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +require 'spec_helper' + +describe Salmon::EncryptedSalmonSlap do + let(:post){ alice.post :status_message, :text => "hi", :to => alice.aspects.create(:name => "sdg").id } + + let!(:created_salmon) {Salmon::EncryptedSalmonSlap.create(alice, post.to_diaspora_xml)} + + describe '#create' do + + it 'has data in the magic envelope' do + created_salmon.magic_sig.data.should_not be nil + end + + it 'has no parsed_data' do + created_salmon.parsed_data.should be nil + end + + it 'sets aes and iv key' do + created_salmon.aes_key.should_not be nil + created_salmon.iv.should_not be nil + end + + it 'makes the data in the signature encrypted with that key' do + key_hash = {'key' => created_salmon.aes_key, 'iv' => created_salmon.iv} + decoded_string = Salmon::EncryptedSalmonSlap.decode64url(created_salmon.magic_sig.data) + alice.aes_decrypt(decoded_string, key_hash).should == post.to_diaspora_xml + end + end + + describe '#xml_for' do + let(:xml) {created_salmon.xml_for eve.person} + + it 'has a encrypted header field' do + xml.include?("encrypted_header").should be true + end + + it 'the encrypted_header field should contain the aes key' do + doc = Nokogiri::XML(xml) + decrypted_header = eve.decrypt(doc.search('encrypted_header').text) + decrypted_header.include?(created_salmon.aes_key).should be true + end + end + + context 'marshaling' do + let(:xml) {created_salmon.xml_for eve.person} + let(:parsed_salmon) { Salmon::EncryptedSalmonSlap.parse(xml, eve)} + + it 'should parse out the aes key' do + parsed_salmon.aes_key.should == created_salmon.aes_key + end + + it 'should parse out the iv' do + parsed_salmon.iv.should == created_salmon.iv + end + it 'should parse out the authors diaspora_handle' do + parsed_salmon.author_email.should == alice.person.diaspora_handle + + end + + describe '#author' do + it 'should reference a local author' do + parsed_salmon.author.should == alice.person + end + + it 'should fail if no author is found' do + parsed_salmon.author_email = 'tom@tom.joindiaspora.com' + + + proc {parsed_salmon.author.public_key}.should raise_error "did you remember to async webfinger?" + + end + + end + + it 'verifies the signature for the sender' do + parsed_salmon.verified_for_key?(alice.public_key).should be true + end + + it 'contains the original data' do + parsed_salmon.parsed_data.should == post.to_diaspora_xml + end + + end +end + diff --git a/spec/lib/salmon/magic_sig_envelope_spec.rb b/spec/lib/salmon/magic_sig_envelope_spec.rb new file mode 100644 index 000000000..1c1236a58 --- /dev/null +++ b/spec/lib/salmon/magic_sig_envelope_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Salmon::MagicSigEnvelope do + +end diff --git a/spec/lib/salmon/salmon_slap_spec.rb b/spec/lib/salmon/salmon_slap_spec.rb new file mode 100644 index 000000000..77f61c492 --- /dev/null +++ b/spec/lib/salmon/salmon_slap_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Salmon::SalmonSlap do + +end From 34ed07260f7c873cb0181921d1b30f2ee32e813b Mon Sep 17 00:00:00 2001 From: Maxwell Salzberg Date: Thu, 8 Sep 2011 19:22:23 -0700 Subject: [PATCH 3/8] MS IZ DG moar salmon refactoring --- app/models/job/http_multi.rb | 2 +- app/models/user.rb | 2 +- lib/postzord/dispatch.rb | 2 +- lib/postzord/receiver.rb | 2 +- ...ypted_salmon_slap.rb => encrypted_slap.rb} | 7 +++++- lib/salmon/magic_sig_envelope.rb | 6 +++-- lib/salmon/salmon.rb | 6 ++--- lib/salmon/{salmon_slap.rb => slap.rb} | 24 ++++++++++++------- spec/controllers/publics_controller_spec.rb | 2 +- spec/lib/postzord/dispatch_spec.rb | 2 +- spec/lib/postzord/receiver_spec.rb | 4 ++-- ...on_slap_spec.rb => encrypted_slap_spec.rb} | 8 +++---- spec/lib/salmon/salmon_slap_spec.rb | 5 ---- spec/lib/salmon/slap_spec.rb | 16 +++++++++++++ spec/models/jobs/http_multi_spec.rb | 4 ++-- 15 files changed, 59 insertions(+), 33 deletions(-) rename lib/salmon/{encrypted_salmon_slap.rb => encrypted_slap.rb} (78%) rename lib/salmon/{salmon_slap.rb => slap.rb} (90%) rename spec/lib/salmon/{encrypted_salmon_slap_spec.rb => encrypted_slap_spec.rb} (88%) delete mode 100644 spec/lib/salmon/salmon_slap_spec.rb create mode 100644 spec/lib/salmon/slap_spec.rb diff --git a/app/models/job/http_multi.rb b/app/models/job/http_multi.rb index 40068c966..3b7b749da 100644 --- a/app/models/job/http_multi.rb +++ b/app/models/job/http_multi.rb @@ -19,7 +19,7 @@ module Job people = Person.where(:id => person_ids) - salmon = Salmon::EncryptedSalmonSlap.create(user, Base64.decode64(enc_object_xml)) + salmon = Salmon::EncryptedSlap.create(user, Base64.decode64(enc_object_xml)) failed_request_people = [] diff --git a/app/models/user.rb b/app/models/user.rb index 31731d743..fb7342248 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -215,7 +215,7 @@ class User < ActiveRecord::Base end def salmon(post) - Salmon::EncryptedSalmonSlap.create(self, post.to_diaspora_xml) + Salmon::EncryptedSlap.create(self, post.to_diaspora_xml) end def build_relayable(model, options = {}) diff --git a/lib/postzord/dispatch.rb b/lib/postzord/dispatch.rb index 81c61e35c..3b12e207a 100644 --- a/lib/postzord/dispatch.rb +++ b/lib/postzord/dispatch.rb @@ -18,7 +18,7 @@ class Postzord::Dispatch end def salmon - @salmon_factory ||= Salmon::EncryptedSalmonSlap.create(@sender, @xml) + @salmon_factory ||= Salmon::EncryptedSlap.create(@sender, @xml) end def post(opts = {}) diff --git a/lib/postzord/receiver.rb b/lib/postzord/receiver.rb index 4a49647ad..f39efd410 100644 --- a/lib/postzord/receiver.rb +++ b/lib/postzord/receiver.rb @@ -45,7 +45,7 @@ module Postzord protected def salmon - @salmon ||= Salmon::EncryptedSalmonSlap.parse(@salmon_xml, @user) + @salmon ||= Salmon::EncryptedSlap.parse(@salmon_xml, @user) end def xml_author diff --git a/lib/salmon/encrypted_salmon_slap.rb b/lib/salmon/encrypted_slap.rb similarity index 78% rename from lib/salmon/encrypted_salmon_slap.rb rename to lib/salmon/encrypted_slap.rb index d40a5c390..cf75e4e6f 100644 --- a/lib/salmon/encrypted_salmon_slap.rb +++ b/lib/salmon/encrypted_slap.rb @@ -3,7 +3,7 @@ # the COPYRIGHT file. module Salmon - class EncryptedSalmonSlap < SalmonSlap + class EncryptedSlap < Slap def header(person) < @@ -21,5 +21,10 @@ XML header = user.decrypt(doc.search('encrypted_header').text) Nokogiri::XML(header) end + + # @return [String] + def self.payload(activity, user, aes_key_hash) + user.person.aes_encrypt(activity, aes_key_hash) + end end end diff --git a/lib/salmon/magic_sig_envelope.rb b/lib/salmon/magic_sig_envelope.rb index cd7b9c34f..c60802ddc 100644 --- a/lib/salmon/magic_sig_envelope.rb +++ b/lib/salmon/magic_sig_envelope.rb @@ -16,13 +16,14 @@ module Salmon env.data = doc.search('//me:env/me:data', ns).text env.alg = doc.search('//me:env/me:alg', ns).text.strip - env.sig = doc.search('//me:env/me:sig', ns).text - env.data_type = doc.search('//me:env/me:data', ns).first['type'].strip unless 'RSA-SHA256' == env.alg raise ArgumentError, "Magic Signature data must be signed with RSA-SHA256, was #{env.alg}" end + env.sig = doc.search('//me:env/me:sig', ns).text + env.data_type = doc.search('//me:env/me:data', ns).first['type'].strip + env end @@ -34,6 +35,7 @@ module Salmon env.encoding = env.get_encoding env.alg = env.get_alg + #TODO: WHY DO WE DOUBLE ENCODE env.sig = Base64.urlsafe_encode64( user.encryption_key.sign OpenSSL::Digest::SHA256.new, env.signable_string ) diff --git a/lib/salmon/salmon.rb b/lib/salmon/salmon.rb index 0099fe0b4..1affeb986 100644 --- a/lib/salmon/salmon.rb +++ b/lib/salmon/salmon.rb @@ -39,7 +39,7 @@ end # Verify documents secured with Magic Signatures module Salmon - autoload :SalmonSlap, File.join(Rails.root, "lib", "salmon", "salmon_slap") - autoload :EncryptedSalmonSlap, File.join(Rails.root, "lib", "salmon", "encrypted_salmon_slap") - autoload :MagicSigEnvelope, File.join(Rails.root, "lib", "salmon", "magic_sig_envelope") + autoload :Slap, File.join(Rails.root, "lib", "salmon", "slap") + autoload :EncryptedSlap, File.join(Rails.root, "lib", "salmon", "encrypted_slap") + autoload :MagicSigEnvelope, File.join(Rails.root, "lib", "salmon", "magic_sig_envelope") end diff --git a/lib/salmon/salmon_slap.rb b/lib/salmon/slap.rb similarity index 90% rename from lib/salmon/salmon_slap.rb rename to lib/salmon/slap.rb index 3a7201662..4f2ffd211 100644 --- a/lib/salmon/salmon_slap.rb +++ b/lib/salmon/slap.rb @@ -3,17 +3,20 @@ # the COPYRIGHT file. module Salmon - class SalmonSlap + class Slap attr_accessor :magic_sig, :author, :author_email, :aes_key, :iv, :parsed_data, :data_type, :sig def self.create(user, activity) salmon = self.new - salmon.author = user.person - aes_key_hash = user.person.gen_aes_key - salmon.aes_key = aes_key_hash['key'] - salmon.iv = aes_key_hash['iv'] - salmon.magic_sig = MagicSigEnvelope.create(user , user.person.aes_encrypt(activity, aes_key_hash)) + salmon.author = user.person + aes_key_hash = user.person.gen_aes_key + + #additional headers + salmon.aes_key = aes_key_hash['key'] + salmon.iv = aes_key_hash['iv'] + + salmon.magic_sig = MagicSigEnvelope.create(user, self.payload(activity, user, aes_key_hash)) salmon end @@ -39,9 +42,14 @@ module Salmon slap end + # @return [String] + def self.payload(activity, user=nil, aes_key_hash=nil) + activity + end + # @return [String] def parse_data(key_hash, user=nil) - SalmonSlap.decode64url(self.magic_sig.data) + Slap.decode64url(self.magic_sig.data) end # @return [Nokogiri::Doc] @@ -55,7 +63,7 @@ module Salmon #{header(person)} #{@magic_sig.to_xml} - + ENTRY end diff --git a/spec/controllers/publics_controller_spec.rb b/spec/controllers/publics_controller_spec.rb index 16192a4a9..1616f09fc 100644 --- a/spec/controllers/publics_controller_spec.rb +++ b/spec/controllers/publics_controller_spec.rb @@ -57,7 +57,7 @@ describe PublicsController do xml2 = post1.to_diaspora_xml user2 = Factory(:user) - salmon_factory = Salmon::SalmonSlap.create(@user, xml2) + salmon_factory = Salmon::EncryptedSlap.create(@user, xml2) enc_xml = salmon_factory.xml_for(user2.person) Resque.should_receive(:enqueue).with(Job::ReceiveSalmon, @user.id, enc_xml).once diff --git a/spec/lib/postzord/dispatch_spec.rb b/spec/lib/postzord/dispatch_spec.rb index da8181c54..de6b1a60d 100644 --- a/spec/lib/postzord/dispatch_spec.rb +++ b/spec/lib/postzord/dispatch_spec.rb @@ -231,7 +231,7 @@ describe Postzord::Dispatch do it 'calls salmon_for each remote person' do salmon = @mailman.salmon - Salmon::SalmonSlap.stub(:create).and_return(salmon) + Salmon::EncryptedSlap.stub(:create).and_return(salmon) salmon.should_receive(:xml_for).with(alice.person).and_return('what') @hydra.stub!(:queue) @hydra.stub!(:run) diff --git a/spec/lib/postzord/receiver_spec.rb b/spec/lib/postzord/receiver_spec.rb index 3b3b24d32..434073fb4 100644 --- a/spec/lib/postzord/receiver_spec.rb +++ b/spec/lib/postzord/receiver_spec.rb @@ -24,7 +24,7 @@ describe Postzord::Receiver do describe '.initialize' do it 'valid for local' do Webfinger.should_not_receive(:new) - Salmon::SalmonSlap.should_not_receive(:parse) + Salmon::EncryptedSlap.should_not_receive(:parse) zord = Postzord::Receiver.new(@user, :person => @person2, :object => @original_post) zord.instance_variable_get(:@user).should_not be_nil @@ -37,7 +37,7 @@ describe Postzord::Receiver do web_mock = mock() web_mock.should_receive(:fetch).and_return true salmon_mock.should_receive(:author_email).and_return(true) - Salmon::SalmonSlap.should_receive(:parse).with(@salmon_xml, @user).and_return(salmon_mock) + Salmon::EncryptedSlap.should_receive(:parse).with(@salmon_xml, @user).and_return(salmon_mock) Webfinger.should_receive(:new).and_return(web_mock) zord = Postzord::Receiver.new(@user, :salmon_xml => @salmon_xml) diff --git a/spec/lib/salmon/encrypted_salmon_slap_spec.rb b/spec/lib/salmon/encrypted_slap_spec.rb similarity index 88% rename from spec/lib/salmon/encrypted_salmon_slap_spec.rb rename to spec/lib/salmon/encrypted_slap_spec.rb index 03b5dd2fb..99aac03b4 100644 --- a/spec/lib/salmon/encrypted_salmon_slap_spec.rb +++ b/spec/lib/salmon/encrypted_slap_spec.rb @@ -4,10 +4,10 @@ require 'spec_helper' -describe Salmon::EncryptedSalmonSlap do +describe Salmon::EncryptedSlap do let(:post){ alice.post :status_message, :text => "hi", :to => alice.aspects.create(:name => "sdg").id } - let!(:created_salmon) {Salmon::EncryptedSalmonSlap.create(alice, post.to_diaspora_xml)} + let!(:created_salmon) {Salmon::EncryptedSlap.create(alice, post.to_diaspora_xml)} describe '#create' do @@ -26,7 +26,7 @@ describe Salmon::EncryptedSalmonSlap do it 'makes the data in the signature encrypted with that key' do key_hash = {'key' => created_salmon.aes_key, 'iv' => created_salmon.iv} - decoded_string = Salmon::EncryptedSalmonSlap.decode64url(created_salmon.magic_sig.data) + decoded_string = Salmon::EncryptedSlap.decode64url(created_salmon.magic_sig.data) alice.aes_decrypt(decoded_string, key_hash).should == post.to_diaspora_xml end end @@ -47,7 +47,7 @@ describe Salmon::EncryptedSalmonSlap do context 'marshaling' do let(:xml) {created_salmon.xml_for eve.person} - let(:parsed_salmon) { Salmon::EncryptedSalmonSlap.parse(xml, eve)} + let(:parsed_salmon) { Salmon::EncryptedSlap.parse(xml, eve)} it 'should parse out the aes key' do parsed_salmon.aes_key.should == created_salmon.aes_key diff --git a/spec/lib/salmon/salmon_slap_spec.rb b/spec/lib/salmon/salmon_slap_spec.rb deleted file mode 100644 index 77f61c492..000000000 --- a/spec/lib/salmon/salmon_slap_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'spec_helper' - -describe Salmon::SalmonSlap do - -end diff --git a/spec/lib/salmon/slap_spec.rb b/spec/lib/salmon/slap_spec.rb new file mode 100644 index 000000000..7d1d076dc --- /dev/null +++ b/spec/lib/salmon/slap_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe Salmon::Slap do + + before do + @post = alice.post(:status_message, :text => "hi", :to => alice.aspects.create(:name => "abcd").id) + @created_salmon = Salmon::Slap.create(alice, @post.to_diaspora_xml) + end + + it 'works' do + salmon_string = @created_salmon.xml_for(nil) + salmon = Salmon::Slap.parse(salmon_string) + salmon.author.should == alice.person + salmon.parsed_data.should == @post.to_diaspora_xml + end +end diff --git a/spec/models/jobs/http_multi_spec.rb b/spec/models/jobs/http_multi_spec.rb index 257d6ec8f..941e4b6d2 100644 --- a/spec/models/jobs/http_multi_spec.rb +++ b/spec/models/jobs/http_multi_spec.rb @@ -60,8 +60,8 @@ describe Job::HttpMulti do Typhoeus::Hydra.stub!(:new).and_return(@hydra) - salmon = Salmon::SalmonSlap.create(bob, Base64.decode64(@post_xml)) - Salmon::SalmonSlap.stub(:create).and_return(salmon) + salmon = Salmon::EncryptedSlap.create(bob, Base64.decode64(@post_xml)) + Salmon::EncryptedSlap.stub(:create).and_return(salmon) salmon.should_receive(:xml_for).and_return("encrypted things") Job::HttpMulti.perform(bob.id, @post_xml, [person.id]) From 6dede984e93806403cc4ea825c201729d6dcb68e Mon Sep 17 00:00:00 2001 From: Maxwell Salzberg Date: Fri, 9 Sep 2011 11:45:51 -0700 Subject: [PATCH 4/8] delegate instead of denormalize --- lib/salmon/slap.rb | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/salmon/slap.rb b/lib/salmon/slap.rb index 4f2ffd211..ad3f7b285 100644 --- a/lib/salmon/slap.rb +++ b/lib/salmon/slap.rb @@ -4,8 +4,8 @@ module Salmon class Slap - attr_accessor :magic_sig, :author, :author_email, :aes_key, :iv, :parsed_data, - :data_type, :sig + attr_accessor :magic_sig, :author, :author_email, :parsed_data + attr_accessor :aes_key, :iv def self.create(user, activity) salmon = self.new @@ -24,24 +24,31 @@ module Salmon slap = self.new doc = Nokogiri::XML(xml) - sig_doc = doc.search('entry') - ### Header ## header_doc = slap.salmon_header(doc, user) slap.author_email= header_doc.search('uri').text.split("acct:").last slap.aes_key = header_doc.search('aes_key').text slap.iv = header_doc.search('iv').text - slap.magic_sig = MagicSigEnvelope.parse sig_doc + slap.magic_sig = MagicSigEnvelope.parse(doc.search('entry')) + + #should be in encrypted salmon only key_hash = {'key' => slap.aes_key, 'iv' => slap.iv} + slap.parsed_data = slap.parse_data(key_hash, user) - slap.sig = slap.magic_sig.sig - slap.data_type = slap.magic_sig.data_type slap end + def sig + self.magic_sig.sig + end + + def data_type + self.magic_sig.data_type + end + # @return [String] def self.payload(activity, user=nil, aes_key_hash=nil) activity From 87c994fa473dd6610c2c50178e00cfc918c197a2 Mon Sep 17 00:00:00 2001 From: Maxwell Salzberg Date: Fri, 9 Sep 2011 13:28:48 -0700 Subject: [PATCH 5/8] MS DG cleaned up and fleshed out salmon specs --- app/models/job/http_multi.rb | 2 +- app/models/user.rb | 2 +- lib/salmon/magic_sig_envelope.rb | 3 +- lib/salmon/slap.rb | 22 +++---- spec/lib/salmon/encrypted_slap_spec.rb | 86 ++++++++------------------ spec/lib/salmon/slap_spec.rb | 53 +++++++++++++++- spec/models/jobs/http_multi_spec.rb | 4 +- 7 files changed, 93 insertions(+), 79 deletions(-) diff --git a/app/models/job/http_multi.rb b/app/models/job/http_multi.rb index 3b7b749da..62b763952 100644 --- a/app/models/job/http_multi.rb +++ b/app/models/job/http_multi.rb @@ -19,7 +19,7 @@ module Job people = Person.where(:id => person_ids) - salmon = Salmon::EncryptedSlap.create(user, Base64.decode64(enc_object_xml)) + salmon = Salmon::EncryptedSlap.create_by_user_and_activity(user, Base64.decode64(enc_object_xml)) failed_request_people = [] diff --git a/app/models/user.rb b/app/models/user.rb index fb7342248..3fc69e017 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -215,7 +215,7 @@ class User < ActiveRecord::Base end def salmon(post) - Salmon::EncryptedSlap.create(self, post.to_diaspora_xml) + Salmon::EncryptedSlap.create_by_user_and_activity(self, post.to_diaspora_xml) end def build_relayable(model, options = {}) diff --git a/lib/salmon/magic_sig_envelope.rb b/lib/salmon/magic_sig_envelope.rb index c60802ddc..ae93bdc39 100644 --- a/lib/salmon/magic_sig_envelope.rb +++ b/lib/salmon/magic_sig_envelope.rb @@ -6,12 +6,13 @@ module Salmon class MagicSigEnvelope attr_accessor :data, :data_type, :encoding, :alg, :sig, :author def self.parse(doc) + puts doc.to_s env = self.new ns = {'me'=>'http://salmon-protocol.org/ns/magic-env'} env.encoding = doc.search('//me:env/me:encoding', ns).text.strip if env.encoding != 'base64url' - raise ArgumentError, "Magic Signature data must be encoded with base64url, was #{slap.magic_sig.encoding}" + raise ArgumentError, "Magic Signature data must be encoded with base64url, was #{env.encoding}" end env.data = doc.search('//me:env/me:data', ns).text diff --git a/lib/salmon/slap.rb b/lib/salmon/slap.rb index ad3f7b285..1e98c839d 100644 --- a/lib/salmon/slap.rb +++ b/lib/salmon/slap.rb @@ -7,7 +7,9 @@ module Salmon attr_accessor :magic_sig, :author, :author_email, :parsed_data attr_accessor :aes_key, :iv - def self.create(user, activity) + delegate :sig, :data_type, :to => :magic_sig + + def self.create_by_user_and_activity(user, activity) salmon = self.new salmon.author = user.person aes_key_hash = user.person.gen_aes_key @@ -20,34 +22,30 @@ module Salmon salmon end - def self.parse(xml, user=nil) + def self.from_xml(xml, receiving_user=nil) slap = self.new doc = Nokogiri::XML(xml) + entry_doc = doc.search('entry') + ### Header ## - header_doc = slap.salmon_header(doc, user) + header_doc = slap.salmon_header(doc, receiving_user) slap.author_email= header_doc.search('uri').text.split("acct:").last + slap.aes_key = header_doc.search('aes_key').text slap.iv = header_doc.search('iv').text - slap.magic_sig = MagicSigEnvelope.parse(doc.search('entry')) + slap.magic_sig = MagicSigEnvelope.parse(entry_doc) #should be in encrypted salmon only key_hash = {'key' => slap.aes_key, 'iv' => slap.iv} - slap.parsed_data = slap.parse_data(key_hash, user) + slap.parsed_data = slap.parse_data(key_hash, receiving_user) slap end - def sig - self.magic_sig.sig - end - - def data_type - self.magic_sig.data_type - end # @return [String] def self.payload(activity, user=nil, aes_key_hash=nil) diff --git a/spec/lib/salmon/encrypted_slap_spec.rb b/spec/lib/salmon/encrypted_slap_spec.rb index 99aac03b4..4f6ea88bd 100644 --- a/spec/lib/salmon/encrypted_slap_spec.rb +++ b/spec/lib/salmon/encrypted_slap_spec.rb @@ -5,34 +5,43 @@ require 'spec_helper' describe Salmon::EncryptedSlap do - let(:post){ alice.post :status_message, :text => "hi", :to => alice.aspects.create(:name => "sdg").id } - - let!(:created_salmon) {Salmon::EncryptedSlap.create(alice, post.to_diaspora_xml)} + before do + @post = alice.post(:status_message, :text => "hi", :to => alice.aspects.create(:name => "abcd").id) + @created_salmon = Salmon::EncryptedSlap.create_by_user_and_activity(alice, @post.to_diaspora_xml) + end describe '#create' do - - it 'has data in the magic envelope' do - created_salmon.magic_sig.data.should_not be nil - end - - it 'has no parsed_data' do - created_salmon.parsed_data.should be nil + it 'makes the data in the signature encrypted with that key' do + key_hash = {'key' => @created_salmon.aes_key, 'iv' => @created_salmon.iv} + decoded_string = Salmon::EncryptedSlap.decode64url(@created_salmon.magic_sig.data) + alice.aes_decrypt(decoded_string, key_hash).should == @post.to_diaspora_xml end it 'sets aes and iv key' do - created_salmon.aes_key.should_not be nil - created_salmon.iv.should_not be nil + @created_salmon.aes_key.should_not be_nil + @created_salmon.iv.should_not be_nil + end + end + + context 'marshalling' do + let(:xml) {@created_salmon.xml_for(eve.person)} + let(:parsed_salmon) { Salmon::EncryptedSlap.from_xml(xml, alice)} + + it 'should parse out the aes key' do + parsed_salmon.aes_key.should == @created_salmon.aes_key end - it 'makes the data in the signature encrypted with that key' do - key_hash = {'key' => created_salmon.aes_key, 'iv' => created_salmon.iv} - decoded_string = Salmon::EncryptedSlap.decode64url(created_salmon.magic_sig.data) - alice.aes_decrypt(decoded_string, key_hash).should == post.to_diaspora_xml + it 'should parse out the iv' do + parsed_salmon.iv.should == @created_salmon.iv + end + + it 'contains the original data' do + parsed_salmon.parsed_data.should == @post.to_diaspora_xml end end describe '#xml_for' do - let(:xml) {created_salmon.xml_for eve.person} + let(:xml) {@created_salmon.xml_for eve.person} it 'has a encrypted header field' do xml.include?("encrypted_header").should be true @@ -41,49 +50,8 @@ describe Salmon::EncryptedSlap do it 'the encrypted_header field should contain the aes key' do doc = Nokogiri::XML(xml) decrypted_header = eve.decrypt(doc.search('encrypted_header').text) - decrypted_header.include?(created_salmon.aes_key).should be true + decrypted_header.include?(@created_salmon.aes_key).should be true end end - - context 'marshaling' do - let(:xml) {created_salmon.xml_for eve.person} - let(:parsed_salmon) { Salmon::EncryptedSlap.parse(xml, eve)} - - it 'should parse out the aes key' do - parsed_salmon.aes_key.should == created_salmon.aes_key - end - - it 'should parse out the iv' do - parsed_salmon.iv.should == created_salmon.iv - end - it 'should parse out the authors diaspora_handle' do - parsed_salmon.author_email.should == alice.person.diaspora_handle - - end - - describe '#author' do - it 'should reference a local author' do - parsed_salmon.author.should == alice.person - end - - it 'should fail if no author is found' do - parsed_salmon.author_email = 'tom@tom.joindiaspora.com' - - - proc {parsed_salmon.author.public_key}.should raise_error "did you remember to async webfinger?" - - end - - end - - it 'verifies the signature for the sender' do - parsed_salmon.verified_for_key?(alice.public_key).should be true - end - - it 'contains the original data' do - parsed_salmon.parsed_data.should == post.to_diaspora_xml - end - - end end diff --git a/spec/lib/salmon/slap_spec.rb b/spec/lib/salmon/slap_spec.rb index 7d1d076dc..c1d6cfa85 100644 --- a/spec/lib/salmon/slap_spec.rb +++ b/spec/lib/salmon/slap_spec.rb @@ -1,16 +1,63 @@ require 'spec_helper' describe Salmon::Slap do - before do @post = alice.post(:status_message, :text => "hi", :to => alice.aspects.create(:name => "abcd").id) - @created_salmon = Salmon::Slap.create(alice, @post.to_diaspora_xml) + @created_salmon = Salmon::Slap.create_by_user_and_activity(alice, @post.to_diaspora_xml) + end + + describe '#create' do + it 'has data in the magic envelope' do + @created_salmon.magic_sig.data.should_not be nil + end + + it 'has no parsed_data' do + @created_salmon.parsed_data.should be nil + end + end it 'works' do salmon_string = @created_salmon.xml_for(nil) - salmon = Salmon::Slap.parse(salmon_string) + salmon = Salmon::Slap.from_xml(salmon_string) salmon.author.should == alice.person salmon.parsed_data.should == @post.to_diaspora_xml end + + describe '#author' do + let(:xml) {@created_salmon.xml_for(eve.person)} + let(:parsed_salmon) { Salmon::Slap.from_xml(xml, alice)} + + it 'should reference a local author' do + parsed_salmon.author.should == alice.person + end + + it 'should fail if no author is found' do + parsed_salmon.author_email = 'tom@tom.joindiaspora.com' + expect { + parsed_salmon.author.public_key + }.should raise_error "did you remember to async webfinger?" + end + end + + context 'marshaling' do + let(:xml) {@created_salmon.xml_for(eve.person)} + let(:parsed_salmon) { Salmon::Slap.from_xml(xml)} + + it 'should parse out the authors diaspora_handle' do + parsed_salmon.author_email.should == alice.person.diaspora_handle + end + + it 'verifies the signature for the sender' do + parsed_salmon.verified_for_key?(alice.public_key).should be_true + end + + it 'verifies the signature for the sender' do + parsed_salmon.verified_for_key?(Factory(:person).public_key).should be_false + end + + it 'contains the original data' do + parsed_salmon.parsed_data.should == @post.to_diaspora_xml + end + end end diff --git a/spec/models/jobs/http_multi_spec.rb b/spec/models/jobs/http_multi_spec.rb index 941e4b6d2..1233e0b0c 100644 --- a/spec/models/jobs/http_multi_spec.rb +++ b/spec/models/jobs/http_multi_spec.rb @@ -60,8 +60,8 @@ describe Job::HttpMulti do Typhoeus::Hydra.stub!(:new).and_return(@hydra) - salmon = Salmon::EncryptedSlap.create(bob, Base64.decode64(@post_xml)) - Salmon::EncryptedSlap.stub(:create).and_return(salmon) + salmon = Salmon::EncryptedSlap.create_by_user_and_activity(bob, Base64.decode64(@post_xml)) + Salmon::EncryptedSlap.stub(:create_by_user_and_activity).and_return(salmon) salmon.should_receive(:xml_for).and_return("encrypted things") Job::HttpMulti.perform(bob.id, @post_xml, [person.id]) From 6e60905d0dba7a7b43562d4c7ee689ecc3dff47e Mon Sep 17 00:00:00 2001 From: Maxwell Salzberg Date: Fri, 9 Sep 2011 18:40:26 -0700 Subject: [PATCH 6/8] DG MS major refactor of salmon and corresponding federation logic --- app/controllers/publics_controller.rb | 2 +- app/models/contact.rb | 3 +- app/models/job/receive_local_batch.rb | 48 +--------- app/models/job/receive_public_salmon.rb | 17 ++++ app/models/post_visibility.rb | 16 ++++ app/models/user.rb | 4 + lib/postzord/dispatch.rb | 2 +- lib/postzord/receiver.rb | 2 +- lib/postzord/receiver/local_post_batch.rb | 45 +++++++++ lib/postzord/receiver/public.rb | 49 +++++++++- lib/salmon/magic_sig_envelope.rb | 1 - spec/controllers/publics_controller_spec.rb | 6 +- spec/lib/postzord/dispatch_spec.rb | 2 +- .../receiver/local_post_batch_spec.rb | 93 +++++++++++++++++++ spec/lib/postzord/receiver/public.rb | 32 ------- spec/lib/postzord/receiver/public_spec.rb | 62 +++++++------ spec/lib/postzord/receiver_spec.rb | 4 +- spec/models/jobs/receive_local_batch_spec.rb | 69 +------------- spec/models/post_visibility_spec.rb | 29 ++++++ 19 files changed, 304 insertions(+), 182 deletions(-) create mode 100644 app/models/job/receive_public_salmon.rb create mode 100644 lib/postzord/receiver/local_post_batch.rb create mode 100644 spec/lib/postzord/receiver/local_post_batch_spec.rb delete mode 100644 spec/lib/postzord/receiver/public.rb create mode 100644 spec/models/post_visibility_spec.rb diff --git a/app/controllers/publics_controller.rb b/app/controllers/publics_controller.rb index 3b555bd1e..eb67146b3 100644 --- a/app/controllers/publics_controller.rb +++ b/app/controllers/publics_controller.rb @@ -50,7 +50,7 @@ class PublicsController < ApplicationController end def receive_public - Postzord::Receiver::Public.new(params[:xml]) + Resque.enqueue(Job::ReceivePublicSalmon, params[:xml]) render :nothing => true, :status => :ok end diff --git a/app/models/contact.rb b/app/models/contact.rb index 00b6dde9f..e4e18fd62 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -19,6 +19,8 @@ class Contact < ActiveRecord::Base validates_uniqueness_of :person_id, :scope => :user_id + before_destroy :destroy_notifications + # contact.sharing is true when contact.person is sharing with contact.user scope :sharing, lambda { where(:sharing => true) @@ -33,7 +35,6 @@ class Contact < ActiveRecord::Base sharing.where(:receiving => false) } - before_destroy :destroy_notifications def destroy_notifications Notification.where(:target_type => "Person", :target_id => person_id, diff --git a/app/models/job/receive_local_batch.rb b/app/models/job/receive_local_batch.rb index d7e0801d6..f5689e797 100644 --- a/app/models/job/receive_local_batch.rb +++ b/app/models/job/receive_local_batch.rb @@ -2,56 +2,18 @@ # licensed under the Affero General Public License version 3 or later. See # the COPYRIGHT file. +require File.join(Rails.root, 'lib/postzord/receiver') +require File.join(Rails.root, 'lib/postzord/receiver/local_post_batch') module Job class ReceiveLocalBatch < Base - require File.join(Rails.root, 'lib/postzord/receiver') @queue = :receive + def self.perform(post_id, recipient_user_ids) post = Post.find(post_id) - create_visibilities(post, recipient_user_ids) - socket_to_users(post, recipient_user_ids) if post.respond_to?(:socket_to_user) - notify_mentioned_users(post) - notify_users(post, recipient_user_ids) - end - - def self.create_visibilities(post, recipient_user_ids) - contacts = Contact.where(:user_id => recipient_user_ids, :person_id => post.author_id) - - if postgres? - # Take the naive approach to inserting our new visibilities for now. - contacts.each do |contact| - PostVisibility.find_or_create_by_contact_id_and_post_id(contact.id, post.id) - end - else - # Use a batch insert on mySQL. - new_post_visibilities = contacts.map do |contact| - PostVisibility.new(:contact_id => contact.id, :post_id => post.id) - end - PostVisibility.import(new_post_visibilities) - end - - end - - def self.socket_to_users(post, recipient_user_ids) - recipient_user_ids.each do |id| - post.socket_to_user(id) - end - end - - def self.notify_mentioned_users(post) - post.mentions.each do |mention| - mention.notify_recipient - end - end - - def self.notify_users(post, recipient_user_ids) - if post.respond_to?(:notification_type) - recipient_user_ids.each{|id| - Notification.notify(User.find(id), post, post.author) - } - end + receiver = Postzord::Receiver::LocalPostBatch.new(post, recipient_user_ids) + receiver.perform! end end end diff --git a/app/models/job/receive_public_salmon.rb b/app/models/job/receive_public_salmon.rb new file mode 100644 index 000000000..d7f700576 --- /dev/null +++ b/app/models/job/receive_public_salmon.rb @@ -0,0 +1,17 @@ +# Copyright (c) 2010, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + + +module Job + class ReceivePublicSalmon < Base + require File.join(Rails.root, 'lib/postzord/receiver') + + @queue = :receive + + def self.perform(xml) + receiver = Postzord::Receiver::Public.new(xml) + receiver.perform! + end + end +end diff --git a/app/models/post_visibility.rb b/app/models/post_visibility.rb index 69122f046..086eaa8be 100644 --- a/app/models/post_visibility.rb +++ b/app/models/post_visibility.rb @@ -5,4 +5,20 @@ class PostVisibility < ActiveRecord::Base belongs_to :contact belongs_to :post + + + def self.batch_import(contacts, post) + if postgres? + # Take the naive approach to inserting our new visibilities for now. + contacts.each do |contact| + PostVisibility.find_or_create_by_contact_id_and_post_id(contact.id, post.id) + end + else + # Use a batch insert on mySQL. + new_post_visibilities = contacts.map do |contact| + PostVisibility.new(:contact_id => contact.id, :post_id => post.id) + end + PostVisibility.import(new_post_visibilities) + end + end end diff --git a/app/models/user.rb b/app/models/user.rb index 3fc69e017..4b1e8684b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -59,6 +59,10 @@ class User < ActiveRecord::Base :invitation_identifier + def self.all_sharing_with_person(person) + User.joins(:contacts).where(:contacts => {:person_id => person.id}) + end + # @return [User] def self.find_by_invitation(invitation) service = invitation.service diff --git a/lib/postzord/dispatch.rb b/lib/postzord/dispatch.rb index 3b12e207a..a04c65bdf 100644 --- a/lib/postzord/dispatch.rb +++ b/lib/postzord/dispatch.rb @@ -18,7 +18,7 @@ class Postzord::Dispatch end def salmon - @salmon_factory ||= Salmon::EncryptedSlap.create(@sender, @xml) + @salmon_factory ||= Salmon::EncryptedSlap.create_by_user_and_activity(@sender, @xml) end def post(opts = {}) diff --git a/lib/postzord/receiver.rb b/lib/postzord/receiver.rb index f39efd410..eb41080fe 100644 --- a/lib/postzord/receiver.rb +++ b/lib/postzord/receiver.rb @@ -45,7 +45,7 @@ module Postzord protected def salmon - @salmon ||= Salmon::EncryptedSlap.parse(@salmon_xml, @user) + @salmon ||= Salmon::EncryptedSlap.from_xml(@salmon_xml, @user) end def xml_author diff --git a/lib/postzord/receiver/local_post_batch.rb b/lib/postzord/receiver/local_post_batch.rb new file mode 100644 index 000000000..7c035a142 --- /dev/null +++ b/lib/postzord/receiver/local_post_batch.rb @@ -0,0 +1,45 @@ +module Postzord + class Receiver + class LocalPostBatch + attr_reader :post, :recipient_user_ids, :users + + def initialize(post, recipient_user_ids) + @post = post + @recipient_user_ids = recipient_user_ids + @users = User.where(:id => @recipient_user_ids) + end + + def perform! + create_visibilities + socket_to_users if @post.respond_to?(:socket_to_user) + notify_mentioned_users + notify_users + end + + def create_visibilities + contacts = Contact.where(:user_id => @recipient_user_ids, :person_id => @post.author_id) + PostVisibility.batch_import(contacts, post) + end + + def socket_to_users + @users.each do |user| + @post.socket_to_user(user) + end + end + + def notify_mentioned_users + @post.mentions.each do |mention| + mention.notify_recipient + end + end + + def notify_users + if @post.respond_to?(:notification_type) + @users.each do |user| + Notification.notify(user, @post, @post.author) + end + end + end + end + end +end diff --git a/lib/postzord/receiver/public.rb b/lib/postzord/receiver/public.rb index c2b6199ec..bc007ecf0 100644 --- a/lib/postzord/receiver/public.rb +++ b/lib/postzord/receiver/public.rb @@ -5,12 +5,55 @@ module Postzord class Receiver class Public - attr_accessor :xml + attr_accessor :salmon, :author def initialize(xml) - @xml = xml - + @salmon = Salmon::Slap.from_xml(xml) + @author = Webfinger.new(@salmon.author_email).fetch end + + # @return [Boolean] + def verified_signature? + @salmon.verified_for_key?(@author.public_key) + end + + # @return [void] + def perform! + return false unless verified_signature? + return unless save_object + + if @object.respond_to?(:relayable) + receive_relayable + else + Resque.enqueue(Job::ReceiveLocalBatch, @object.id, self.recipient_user_ids) + end + end + + def receive_relayable + raise RelayableObjectWithoutParent.new("Receiving a relayable object without parent object present locally!") unless @object.parent.user.present? + + # receive relayable object only for the owner of the parent object + @object.receive(@object.parent.user, @author) + + # notify everyone who can see the parent object + receiver = Postzord::Receiver::LocalPostBatch.new(nil, self.recipient_user_ids) + receiver.notify_users + end + + # @return [Object] + def save_object + @object = Diaspora::Parser::from_xml(@salmon.parsed_data) + raise "Object is not public" unless @object.public? + @object.save! + end + + # @return [Array] User ids + def recipient_user_ids + User.all_sharing_with_person(@author).select('users.id').map!{ |u| u.id } + end + + class RelayableObjectWithoutParent < StandardError ; ; end end end end + diff --git a/lib/salmon/magic_sig_envelope.rb b/lib/salmon/magic_sig_envelope.rb index ae93bdc39..063555b12 100644 --- a/lib/salmon/magic_sig_envelope.rb +++ b/lib/salmon/magic_sig_envelope.rb @@ -6,7 +6,6 @@ module Salmon class MagicSigEnvelope attr_accessor :data, :data_type, :encoding, :alg, :sig, :author def self.parse(doc) - puts doc.to_s env = self.new ns = {'me'=>'http://salmon-protocol.org/ns/magic-env'} env.encoding = doc.search('//me:env/me:encoding', ns).text.strip diff --git a/spec/controllers/publics_controller_spec.rb b/spec/controllers/publics_controller_spec.rb index 1616f09fc..5e48b0e74 100644 --- a/spec/controllers/publics_controller_spec.rb +++ b/spec/controllers/publics_controller_spec.rb @@ -31,9 +31,9 @@ describe PublicsController do response.code.should == '422' end - it 'calls Postzord::Receiver:Public' do + it 'enqueues a ReceivePublicSalmon job' do xml = "stuff" - Postzord::Receiver::Public.should_receive(:new).with(xml) + Resque.should_receive(:enqueue).with(Job::ReceivePublicSalmon, xml) post :receive_public, :xml => xml end end @@ -57,7 +57,7 @@ describe PublicsController do xml2 = post1.to_diaspora_xml user2 = Factory(:user) - salmon_factory = Salmon::EncryptedSlap.create(@user, xml2) + salmon_factory = Salmon::EncryptedSlap.create_by_user_and_activity(@user, xml2) enc_xml = salmon_factory.xml_for(user2.person) Resque.should_receive(:enqueue).with(Job::ReceiveSalmon, @user.id, enc_xml).once diff --git a/spec/lib/postzord/dispatch_spec.rb b/spec/lib/postzord/dispatch_spec.rb index de6b1a60d..68a99eb33 100644 --- a/spec/lib/postzord/dispatch_spec.rb +++ b/spec/lib/postzord/dispatch_spec.rb @@ -231,7 +231,7 @@ describe Postzord::Dispatch do it 'calls salmon_for each remote person' do salmon = @mailman.salmon - Salmon::EncryptedSlap.stub(:create).and_return(salmon) + Salmon::EncryptedSlap.stub(:create_by_user_and_activity).and_return(salmon) salmon.should_receive(:xml_for).with(alice.person).and_return('what') @hydra.stub!(:queue) @hydra.stub!(:run) diff --git a/spec/lib/postzord/receiver/local_post_batch_spec.rb b/spec/lib/postzord/receiver/local_post_batch_spec.rb new file mode 100644 index 000000000..89db17c83 --- /dev/null +++ b/spec/lib/postzord/receiver/local_post_batch_spec.rb @@ -0,0 +1,93 @@ +require 'spec_helper' +require File.join(Rails.root, 'lib','postzord', 'receiver', 'local_post_batch') + +describe Postzord::Receiver::LocalPostBatch do + before do + @post = Factory(:status_message, :author => alice.person) + @ids = [bob.id] + + @receiver = Postzord::Receiver::LocalPostBatch.new(@post, @ids) + end + + describe '.initialize' do + it 'sets @post, @recipient_user_ids, and @user' do + [:post, :recipient_user_ids, :users].each do |instance_var| + @receiver.send(instance_var).should_not be_nil + end + end + end + + describe '#perform!' do + it 'calls .create_visibilities' do + @receiver.should_receive(:create_visibilities) + @receiver.perform! + end + + it 'sockets to users' do + @receiver.should_receive(:socket_to_users) + @receiver.perform! + end + + it 'notifies mentioned users' do + @receiver.should_receive(:notify_mentioned_users) + @receiver.perform! + end + + it 'notifies users' do + @receiver.should_receive(:notify_users) + @receiver.perform! + end + end + + describe '#create_visibilities' do + it 'calls Postvisibility.batch_import' do + PostVisibility.should_receive(:batch_import) + @receiver.create_visibilities + end + end + + describe '#socket_to_users' do + before do + @controller = mock() + SocketsController.stub(:new).and_return(@controller) + end + + it 'sockets to each user' do + @controller.should_receive(:outgoing).with(bob, @post, instance_of(Hash)) + @receiver.socket_to_users + end + end + + describe '#notify_mentioned_users' do + it 'calls notify person for a mentioned person' do + sm = Factory(:status_message, + :author => alice.person, + :text => "Hey @{Bob; #{bob.diaspora_handle}}") + + receiver = Postzord::Receiver::LocalPostBatch.new(sm, @ids) + Notification.should_receive(:notify).with(bob, anything, alice.person) + receiver.notify_mentioned_users + end + + it 'does not call notify person for a non-mentioned person' do + Notification.should_not_receive(:notify) + @receiver.notify_mentioned_users + end + end + + describe '#notify_users' do + it 'calls notify for posts with notification type' do + reshare = Factory.create(:reshare) + Notification.should_receive(:notify) + receiver = Postzord::Receiver::LocalPostBatch.new(reshare, @ids) + receiver.notify_users + end + + it 'calls notify for posts with notification type' do + sm = Factory.create(:status_message, :author => alice.person) + receiver = Postzord::Receiver::LocalPostBatch.new(sm, @ids) + Notification.should_not_receive(:notify) + receiver.notify_users + end + end +end diff --git a/spec/lib/postzord/receiver/public.rb b/spec/lib/postzord/receiver/public.rb deleted file mode 100644 index 5b9259d09..000000000 --- a/spec/lib/postzord/receiver/public.rb +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) 2010, Diaspora Inc. This file is -# licensed under the Affero General Public License version 3 or later. See -# the COPYRIGHT file. - -require 'spec_helper' - -require File.join(Rails.root, 'lib/postzord') -require File.join(Rails.root, 'lib/postzord/receiver/public') - -describe Postzord::Receiver::Public do - - describe '#initialize' do - - end - - describe '#verify_signature' do - - end - - describe '#collect_recipients' do - - end - - describe '#batch_insert_visibilities' do - - end - - describe '#batch_notify' do - - end - -end diff --git a/spec/lib/postzord/receiver/public_spec.rb b/spec/lib/postzord/receiver/public_spec.rb index 318cc1a02..7fab3872e 100644 --- a/spec/lib/postzord/receiver/public_spec.rb +++ b/spec/lib/postzord/receiver/public_spec.rb @@ -8,52 +8,60 @@ require File.join(Rails.root, 'lib/postzord') require File.join(Rails.root, 'lib/postzord/receiver/public') describe Postzord::Receiver::Public do + before do + @post = Factory.build(:status_message, :author => alice.person, :public => true) + @created_salmon = Salmon::Slap.create_by_user_and_activity(alice, @post.to_diaspora_xml) + @xml = @created_salmon.xml_for(nil) + end describe '#initialize' do - it 'sets xml as instance variable' do - receiver = Postzord::Receiver::Public.new("blah") - receiver.xml.should == 'blah' + it 'creates a Salmon instance variable' do + receiver = Postzord::Receiver::Public.new(@xml) + receiver.salmon.should_not be_nil end end - describe '#perform' do - it 'calls verify_signature' do + describe '#perform!' do + before do + @receiver = Postzord::Receiver::Public.new(@xml) + end + it 'calls verify_signature' do + @receiver.should_receive(:verified_signature?) + @receiver.perform! end context 'if signature is valid' do - it 'calls collect_recipients' do - + it 'calls recipient_user_ids' do + @receiver.should_receive(:recipient_user_ids) + @receiver.perform! end it 'saves the parsed object' do - + @receiver.should_receive(:save_object) + @receiver.perform! end - it 'calls batch_insert_visibilities' do - - end - - it 'calls batch_notify' do - + it 'enqueues a Job::ReceiveLocalBatch' do + Resque.should_receive(:enqueue).with(Job::ReceiveLocalBatch, anything, anything) + @receiver.perform! end end end - describe '#verify_signature' do - + describe '#verify_signature?' do + it 'calls Slap#verified_for_key?' do + receiver = Postzord::Receiver::Public.new(@xml) + receiver.salmon.should_receive(:verified_for_key?).with(instance_of(OpenSSL::PKey::RSA)) + receiver.verified_signature? + end end - describe '#collect_recipients' do - + describe '#recipient_user_ids' do + it 'calls User.all_sharing_with_person' do + User.should_receive(:all_sharing_with_person).and_return(stub(:select => [])) + receiver = Postzord::Receiver::Public.new(@xml) + receiver.perform! + end end - - describe '#batch_insert_visibilities' do - - end - - describe '#batch_notify' do - - end - end diff --git a/spec/lib/postzord/receiver_spec.rb b/spec/lib/postzord/receiver_spec.rb index 434073fb4..bcc5fe204 100644 --- a/spec/lib/postzord/receiver_spec.rb +++ b/spec/lib/postzord/receiver_spec.rb @@ -24,7 +24,7 @@ describe Postzord::Receiver do describe '.initialize' do it 'valid for local' do Webfinger.should_not_receive(:new) - Salmon::EncryptedSlap.should_not_receive(:parse) + Salmon::EncryptedSlap.should_not_receive(:from_xml) zord = Postzord::Receiver.new(@user, :person => @person2, :object => @original_post) zord.instance_variable_get(:@user).should_not be_nil @@ -37,7 +37,7 @@ describe Postzord::Receiver do web_mock = mock() web_mock.should_receive(:fetch).and_return true salmon_mock.should_receive(:author_email).and_return(true) - Salmon::EncryptedSlap.should_receive(:parse).with(@salmon_xml, @user).and_return(salmon_mock) + Salmon::EncryptedSlap.should_receive(:from_xml).with(@salmon_xml, @user).and_return(salmon_mock) Webfinger.should_receive(:new).and_return(web_mock) zord = Postzord::Receiver.new(@user, :salmon_xml => @salmon_xml) diff --git a/spec/models/jobs/receive_local_batch_spec.rb b/spec/models/jobs/receive_local_batch_spec.rb index a6eb038e0..5b9ba09f2 100644 --- a/spec/models/jobs/receive_local_batch_spec.rb +++ b/spec/models/jobs/receive_local_batch_spec.rb @@ -9,73 +9,10 @@ describe Job::ReceiveLocalBatch do @post = alice.build_post(:status_message, :text => 'Hey Bob') @post.save! end + describe '.perform' do - it 'calls .create_visibilities' do - Job::ReceiveLocalBatch.should_receive(:create_visibilities).with(@post, [bob.id]) - Job::ReceiveLocalBatch.perform(@post.id, [bob.id]) - end - it 'sockets to users' do - Job::ReceiveLocalBatch.should_receive(:socket_to_users).with(@post, [bob.id]) - Job::ReceiveLocalBatch.perform(@post.id, [bob.id]) - end - it 'notifies mentioned users' do - Job::ReceiveLocalBatch.should_receive(:notify_mentioned_users).with(@post) - Job::ReceiveLocalBatch.perform(@post.id, [bob.id]) - end - it 'notifies users' do - Job::ReceiveLocalBatch.should_receive(:notify_users).with(@post, [bob.id]) - Job::ReceiveLocalBatch.perform(@post.id, [bob.id]) - end - end - describe '.create_visibilities' do - it 'creates a visibility for each user' do - PostVisibility.exists?(:contact_id => bob.contact_for(alice.person).id, :post_id => @post.id).should be_false - Job::ReceiveLocalBatch.create_visibilities(@post, [bob.id]) - PostVisibility.exists?(:contact_id => bob.contact_for(alice.person).id, :post_id => @post.id).should be_true - end - it 'does not raise if a visibility already exists' do - PostVisibility.create!(:contact_id => bob.contact_for(alice.person).id, :post_id => @post.id) - lambda { - Job::ReceiveLocalBatch.create_visibilities(@post, [bob.id]) - }.should_not raise_error - end - end - describe '.socket_to_users' do - before do - @controller = mock() - SocketsController.stub(:new).and_return(@controller) - end - it 'sockets to each user' do - @controller.should_receive(:outgoing).with(bob.id, @post, instance_of(Hash)) - Job::ReceiveLocalBatch.socket_to_users(@post, [bob.id]) - end - end - describe '.notify_mentioned_users' do - it 'calls notify person for a mentioned person' do - @post = alice.build_post(:status_message, :text => "Hey @{Bob; #{bob.diaspora_handle}}") - @post.save! - Notification.should_receive(:notify).with(bob, anything, alice.person) - Job::ReceiveLocalBatch.notify_mentioned_users(@post) - end - it 'does not call notify person for a non-mentioned person' do - Notification.should_not_receive(:notify) - Job::ReceiveLocalBatch.notify_mentioned_users(@post) - end - end - - describe '.notify_users' do - it 'calls notify for posts with notification type' do - reshare = Factory.create(:reshare) - Notification.should_receive(:notify) - - Job::ReceiveLocalBatch.notify_users(reshare, [bob.id]) - end - - it 'calls notify for posts with notification type' do - sm = Factory.create(:status_message, :author => alice.person) - Notification.should_not_receive(:notify) - - Job::ReceiveLocalBatch.notify_users(sm, [bob.id]) + it 'calls Postzord::Receiver::LocalPostBatch' do + pending end end end diff --git a/spec/models/post_visibility_spec.rb b/spec/models/post_visibility_spec.rb new file mode 100644 index 000000000..29fc538a4 --- /dev/null +++ b/spec/models/post_visibility_spec.rb @@ -0,0 +1,29 @@ +# Copyright (c) 2010, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +require 'spec_helper' + +describe PostVisibility do + describe '.batch_import' do + before do + @post = Factory(:status_message, :author => alice.person) + @contact = bob.contact_for(alice.person) + end + + it 'creates a visibility for each user' do + lambda { + PostVisibility.batch_import([@contact], @post) + }.should change { + PostVisibility.exists?(:contact_id => @contact.id, :post_id => @post.id) + }.from(false).to(true) + end + + it 'does not raise if a visibility already exists' do + PostVisibility.create!(:contact_id => @contact.id, :post_id => @post.id) + lambda { + PostVisibility.batch_import([@contact], @post) + }.should_not raise_error + end + end +end From 5177e5b2183bd536a8431a4e10ace2dd254081b4 Mon Sep 17 00:00:00 2001 From: Maxwell Salzberg Date: Mon, 12 Sep 2011 08:32:39 -0700 Subject: [PATCH 7/8] DG MS test receive_relayable --- app/models/comment.rb | 12 +++++-- lib/postzord/receiver/public.rb | 22 ++++++++---- spec/lib/postzord/receiver/public_spec.rb | 44 +++++++++++++++++++++++ 3 files changed, 69 insertions(+), 9 deletions(-) diff --git a/app/models/comment.rb b/app/models/comment.rb index e455f1457..7214f7b7b 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -56,15 +56,23 @@ class Comment < ActiveRecord::Base end def notification_type(user, person) - if self.post.author == user.person + if user.owns?(self.post) return Notifications::CommentOnPost - elsif self.post.comments.where(:author_id => user.person.id) != [] && self.author_id != user.person.id + elsif user_has_commented_on_others_post?(person, self.post, user) return Notifications::AlsoCommented else return false end end + def user_has_commented_on_others_post?(author, post, user) + Comment.comments_by_author_on_post_exist?(author, post.id) && self.author_id != user.person.id + end + + def self.comments_by_author_on_post_exist?(author, post_id) + Comment.exists?(:author_id => author.id, :post_id => post_id) + end + def parent_class Post end diff --git a/lib/postzord/receiver/public.rb b/lib/postzord/receiver/public.rb index bc007ecf0..55ef99092 100644 --- a/lib/postzord/receiver/public.rb +++ b/lib/postzord/receiver/public.rb @@ -22,7 +22,7 @@ module Postzord return false unless verified_signature? return unless save_object - if @object.respond_to?(:relayable) + if @object.respond_to?(:relayable?) receive_relayable else Resque.enqueue(Job::ReceiveLocalBatch, @object.id, self.recipient_user_ids) @@ -30,11 +30,14 @@ module Postzord end def receive_relayable - raise RelayableObjectWithoutParent.new("Receiving a relayable object without parent object present locally!") unless @object.parent.user.present? - - # receive relayable object only for the owner of the parent object - @object.receive(@object.parent.user, @author) + # unless @object.parent.present? + # raise RelayableObjectWithoutParent.new("Receiving a relayable object without parent object present locally!") + # end + if @object.parent.author.local? + # receive relayable object only for the owner of the parent object + @object.receive(@object.parent.author.user, @author) + end # notify everyone who can see the parent object receiver = Postzord::Receiver::LocalPostBatch.new(nil, self.recipient_user_ids) receiver.notify_users @@ -43,17 +46,22 @@ module Postzord # @return [Object] def save_object @object = Diaspora::Parser::from_xml(@salmon.parsed_data) - raise "Object is not public" unless @object.public? + raise "Object is not public" if object_can_be_public_and_it_is_not? @object.save! end + # @return [Array] User ids def recipient_user_ids User.all_sharing_with_person(@author).select('users.id').map!{ |u| u.id } end class RelayableObjectWithoutParent < StandardError ; ; end + private + + def object_can_be_public_and_it_is_not? + @object.respond_to?(:public) && !@object.public? + end end end end - diff --git a/spec/lib/postzord/receiver/public_spec.rb b/spec/lib/postzord/receiver/public_spec.rb index 7fab3872e..3afb1241b 100644 --- a/spec/lib/postzord/receiver/public_spec.rb +++ b/spec/lib/postzord/receiver/public_spec.rb @@ -14,6 +14,19 @@ describe Postzord::Receiver::Public do @xml = @created_salmon.xml_for(nil) end + context 'round trips works with' do + it 'a comment' do + comment = bob.build_comment(:text => 'yo', :post => Factory(:status_message)) + comment.save + xml = Salmon::Slap.create_by_user_and_activity(bob, comment.to_diaspora_xml).xml_for(nil) + comment.destroy + expect{ + receiver = Postzord::Receiver::Public.new(xml) + receiver.perform! + }.to change(Comment, :count).by(1) + end + end + describe '#initialize' do it 'creates a Salmon instance variable' do receiver = Postzord::Receiver::Public.new(@xml) @@ -64,4 +77,35 @@ describe Postzord::Receiver::Public do receiver.perform! end end + + describe '#receive_relayable' do + before do + @comment = bob.build_comment(:text => 'yo', :post => Factory(:status_message)) + @comment.save + created_salmon = Salmon::Slap.create_by_user_and_activity(alice, @comment.to_diaspora_xml) + xml = created_salmon.xml_for(nil) + @comment.delete + @receiver = Postzord::Receiver::Public.new(xml) + end + + it 'raises if parent object does not exist' + + it 'receives only for the parent author if he is local to the pod' do + comment = stub.as_null_object + @receiver.instance_variable_set(:@object, comment) + + comment.should_receive(:receive) + @receiver.receive_relayable + end + + it 'calls notifiy_users' do + comment = stub.as_null_object + @receiver.instance_variable_set(:@object, comment) + + local_post_batch_receiver = stub.as_null_object + Postzord::Receiver::LocalPostBatch.stub(:new).and_return(local_post_batch_receiver) + local_post_batch_receiver.should_receive(:notify_users) + @receiver.receive_relayable + end + end end From 0ab0526cd683ddb13c2c370e6fedb1805dea4da3 Mon Sep 17 00:00:00 2001 From: Maxwell Salzberg Date: Mon, 12 Sep 2011 15:06:17 -0700 Subject: [PATCH 8/8] DG MS; put validates_associated in relayable, removed from receiver logic --- app/models/comment.rb | 16 ++++------------ app/models/like.rb | 3 +-- app/models/message.rb | 9 ++++----- app/models/person.rb | 2 +- lib/diaspora/relayable.rb | 3 +++ lib/postzord/receiver/public.rb | 5 ----- spec/models/message_spec.rb | 2 +- spec/shared_behaviors/relayable.rb | 6 ++++++ 8 files changed, 20 insertions(+), 26 deletions(-) diff --git a/app/models/comment.rb b/app/models/comment.rb index 7214f7b7b..41b8b8fd9 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -25,9 +25,9 @@ class Comment < ActiveRecord::Base belongs_to :post belongs_to :author, :class_name => 'Person' - + validates :text, :presence => true, :length => { :maximum => 2500 } - validates :post, :presence => true + validates :parent, :presence => true #should be in relayable (pending on fixing Message) serialize :youtube_titles, Hash @@ -56,23 +56,15 @@ class Comment < ActiveRecord::Base end def notification_type(user, person) - if user.owns?(self.post) + if self.post.author == user.person return Notifications::CommentOnPost - elsif user_has_commented_on_others_post?(person, self.post, user) + elsif self.post.comments.where(:author_id => user.person.id) != [] && self.author_id != user.person.id return Notifications::AlsoCommented else return false end end - def user_has_commented_on_others_post?(author, post, user) - Comment.comments_by_author_on_post_exist?(author, post.id) && self.author_id != user.person.id - end - - def self.comments_by_author_on_post_exist?(author, post_id) - Comment.exists?(:author_id => author.id, :post_id => post_id) - end - def parent_class Post end diff --git a/app/models/like.rb b/app/models/like.rb index 21f9f6194..029d96a08 100644 --- a/app/models/like.rb +++ b/app/models/like.rb @@ -20,8 +20,7 @@ class Like < ActiveRecord::Base belongs_to :author, :class_name => 'Person' validates_uniqueness_of :target_id, :scope => [:target_type, :author_id] - validates :author, :presence => true - validates :target, :presence => true + validates :parent, :presence => true #should be in relayable (pending on fixing Message) after_create do self.parent.update_likes_counter diff --git a/app/models/message.rb b/app/models/message.rb index 16cde59a2..c8741736a 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -13,23 +13,22 @@ class Message < ActiveRecord::Base belongs_to :author, :class_name => 'Person' belongs_to :conversation, :touch => true - + validates :text, :presence => true + validate :participant_of_parent_conversation after_create do #sign comment as commenter self.author_signature = self.sign_with_key(self.author.owner.encryption_key) if self.author.owner - if !self.parent.blank? && self.author.owns?(self.parent) + if self.author.owns?(self.parent) #sign comment as post owner - self.parent_author_signature = self.sign_with_key( self.parent.author.owner.encryption_key) if self.parent.author.owner + self.parent_author_signature = self.sign_with_key(self.parent.author.owner.encryption_key) if self.parent.author.owner end self.save! self end - validate :participant_of_parent_conversation - def diaspora_handle self.author.diaspora_handle end diff --git a/app/models/person.rb b/app/models/person.rb index 6abdd14fc..a36784532 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -158,7 +158,7 @@ class Person < ActiveRecord::Base end def owns?(obj) - self == obj.author + self.id == obj.author_id end def url diff --git a/lib/diaspora/relayable.rb b/lib/diaspora/relayable.rb index a1f51038e..08ad4a8bd 100644 --- a/lib/diaspora/relayable.rb +++ b/lib/diaspora/relayable.rb @@ -12,6 +12,9 @@ module Diaspora xml_attr :parent_guid xml_attr :parent_author_signature xml_attr :author_signature + + validates_associated :parent + validates :author, :presence => true end end diff --git a/lib/postzord/receiver/public.rb b/lib/postzord/receiver/public.rb index 55ef99092..abc7a37de 100644 --- a/lib/postzord/receiver/public.rb +++ b/lib/postzord/receiver/public.rb @@ -30,10 +30,6 @@ module Postzord end def receive_relayable - # unless @object.parent.present? - # raise RelayableObjectWithoutParent.new("Receiving a relayable object without parent object present locally!") - # end - if @object.parent.author.local? # receive relayable object only for the owner of the parent object @object.receive(@object.parent.author.user, @author) @@ -50,7 +46,6 @@ module Postzord @object.save! end - # @return [Array] User ids def recipient_user_ids User.all_sharing_with_person(@author).select('users.id').map!{ |u| u.id } diff --git a/spec/models/message_spec.rb b/spec/models/message_spec.rb index 00aacfb10..c5a5b653e 100644 --- a/spec/models/message_spec.rb +++ b/spec/models/message_spec.rb @@ -17,7 +17,7 @@ describe Message do :messages_attributes => [ {:author => @user1.person, :text => 'stuff'} ] } - @cnv = Conversation.create(@create_hash) + @cnv = Conversation.create!(@create_hash) @message = @cnv.messages.first @xml = @message.to_diaspora_xml end diff --git a/spec/shared_behaviors/relayable.rb b/spec/shared_behaviors/relayable.rb index 36b7873de..16e14aade 100644 --- a/spec/shared_behaviors/relayable.rb +++ b/spec/shared_behaviors/relayable.rb @@ -6,6 +6,12 @@ require 'spec_helper' describe Diaspora::Relayable do shared_examples_for "it is relayable" do + + context 'validation' do + it 'ensures an valid associated parent' + it 'ensures the presence of an author' + end + context 'encryption' do describe '#parent_author_signature' do it 'should sign the object if the user is the post author' do