Merge branch 'public_receive'
This commit is contained in:
commit
27d882b2b6
37 changed files with 847 additions and 433 deletions
|
|
@ -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
|
||||
def receive_public
|
||||
Resque.enqueue(Job::ReceivePublicSalmon, 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
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class Comment < ActiveRecord::Base
|
|||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ module Job
|
|||
|
||||
people = Person.where(:id => person_ids)
|
||||
|
||||
salmon = Salmon::SalmonSlap.create(user, Base64.decode64(enc_object_xml))
|
||||
salmon = Salmon::EncryptedSlap.create_by_user_and_activity(user, Base64.decode64(enc_object_xml))
|
||||
|
||||
failed_request_people = []
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
17
app/models/job/receive_public_salmon.rb
Normal file
17
app/models/job/receive_public_salmon.rb
Normal file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -15,12 +15,13 @@ class Message < ActiveRecord::Base
|
|||
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
|
||||
end
|
||||
|
|
@ -28,8 +29,6 @@ class Message < ActiveRecord::Base
|
|||
self
|
||||
end
|
||||
|
||||
validate :participant_of_parent_conversation
|
||||
|
||||
def diaspora_handle
|
||||
self.author.diaspora_handle
|
||||
end
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ class Person < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def owns?(obj)
|
||||
self == obj.author
|
||||
self.id == obj.author_id
|
||||
end
|
||||
|
||||
def url
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -215,7 +219,7 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def salmon(post)
|
||||
Salmon::SalmonSlap.create(self, post.to_diaspora_xml)
|
||||
Salmon::EncryptedSlap.create_by_user_and_activity(self, post.to_diaspora_xml)
|
||||
end
|
||||
|
||||
def build_relayable(model, options = {})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class Postzord::Dispatch
|
|||
end
|
||||
|
||||
def salmon
|
||||
@salmon_factory ||= Salmon::SalmonSlap.create(@sender, @xml)
|
||||
@salmon_factory ||= Salmon::EncryptedSlap.create_by_user_and_activity(@sender, @xml)
|
||||
end
|
||||
|
||||
def post(opts = {})
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ module Postzord
|
|||
|
||||
protected
|
||||
def salmon
|
||||
@salmon ||= Salmon::SalmonSlap.parse(@salmon_xml, @user)
|
||||
@salmon ||= Salmon::EncryptedSlap.from_xml(@salmon_xml, @user)
|
||||
end
|
||||
|
||||
def xml_author
|
||||
|
|
|
|||
45
lib/postzord/receiver/local_post_batch.rb
Normal file
45
lib/postzord/receiver/local_post_batch.rb
Normal file
|
|
@ -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
|
||||
62
lib/postzord/receiver/public.rb
Normal file
62
lib/postzord/receiver/public.rb
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# 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 :salmon, :author
|
||||
|
||||
def initialize(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
|
||||
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
|
||||
end
|
||||
|
||||
# @return [Object]
|
||||
def save_object
|
||||
@object = Diaspora::Parser::from_xml(@salmon.parsed_data)
|
||||
raise "Object is not public" if object_can_be_public_and_it_is_not?
|
||||
@object.save!
|
||||
end
|
||||
|
||||
# @return [Array<Integer>] 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
|
||||
30
lib/salmon/encrypted_slap.rb
Normal file
30
lib/salmon/encrypted_slap.rb
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# 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 EncryptedSlap < Slap
|
||||
def header(person)
|
||||
<<XML
|
||||
<encrypted_header>
|
||||
#{person.encrypt("<decrypted_header>#{plaintext_header}</decrypted_header>")}
|
||||
</encrypted_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
|
||||
|
||||
# @return [String]
|
||||
def self.payload(activity, user, aes_key_hash)
|
||||
user.person.aes_encrypt(activity, aes_key_hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
72
lib/salmon/magic_sig_envelope.rb
Normal file
72
lib/salmon/magic_sig_envelope.rb
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# 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 #{env.encoding}"
|
||||
end
|
||||
|
||||
env.data = doc.search('//me:env/me:data', ns).text
|
||||
env.alg = doc.search('//me:env/me:alg', ns).text.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
|
||||
|
||||
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
|
||||
|
||||
#TODO: WHY DO WE DOUBLE ENCODE
|
||||
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
|
||||
<<ENTRY
|
||||
<me:env xmlns:me="http://salmon-protocol.org/ns/magic-env">
|
||||
<me:data type='#{@data_type}'>#{@data}</me:data>
|
||||
<me:encoding>#{@encoding}</me:encoding>
|
||||
<me:alg>#{@alg}</me:alg>
|
||||
<me:sig>#{@sig}</me:sig>
|
||||
</me:env>
|
||||
ENTRY
|
||||
end
|
||||
|
||||
def get_encoding
|
||||
'base64url'
|
||||
end
|
||||
|
||||
def get_data_type
|
||||
'application/atom+xml'
|
||||
end
|
||||
|
||||
def get_alg
|
||||
'RSA-SHA256'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -39,205 +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)
|
||||
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)
|
||||
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
|
||||
|
||||
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 xml_for person
|
||||
xml =<<ENTRY
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<entry xmlns='http://www.w3.org/2005/Atom'>
|
||||
<encrypted_header>#{person.encrypt(decrypted_header)}</encrypted_header>
|
||||
#{@magic_sig.to_xml}
|
||||
</entry>
|
||||
ENTRY
|
||||
|
||||
end
|
||||
|
||||
def decrypted_header
|
||||
header =<<HEADER
|
||||
<decrypted_header>
|
||||
<iv>#{iv}</iv>
|
||||
<aes_key>#{aes_key}</aes_key>
|
||||
<author>
|
||||
<name>#{@author.name}</name>
|
||||
<uri>acct:#{@author.diaspora_handle}</uri>
|
||||
</author>
|
||||
</decrypted_header>
|
||||
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
|
||||
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
|
||||
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= <<ENTRY
|
||||
<me:env xmlns:me="http://salmon-protocol.org/ns/magic-env">
|
||||
<me:data type='#{@data_type}'>#{@data}</me:data>
|
||||
<me:encoding>#{@encoding}</me:encoding>
|
||||
<me:alg>#{@alg}</me:alg>
|
||||
<me:sig>#{@sig}</me:sig>
|
||||
</me:env>
|
||||
ENTRY
|
||||
xml
|
||||
end
|
||||
|
||||
def get_encoding
|
||||
'base64url'
|
||||
end
|
||||
|
||||
def get_data_type
|
||||
'application/atom+xml'
|
||||
end
|
||||
|
||||
def get_alg
|
||||
'RSA-SHA256'
|
||||
end
|
||||
|
||||
end
|
||||
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
|
||||
|
|
|
|||
163
lib/salmon/slap.rb
Normal file
163
lib/salmon/slap.rb
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
# 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 Slap
|
||||
attr_accessor :magic_sig, :author, :author_email, :parsed_data
|
||||
attr_accessor :aes_key, :iv
|
||||
|
||||
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
|
||||
|
||||
#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
|
||||
|
||||
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, 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(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, receiving_user)
|
||||
|
||||
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)
|
||||
Slap.decode64url(self.magic_sig.data)
|
||||
end
|
||||
|
||||
# @return [Nokogiri::Doc]
|
||||
def salmon_header(doc, user=nil)
|
||||
doc.search('header')
|
||||
end
|
||||
|
||||
def xml_for(person)
|
||||
xml =<<ENTRY
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<entry xmlns='http://www.w3.org/2005/Atom'>
|
||||
#{header(person)}
|
||||
#{@magic_sig.to_xml}
|
||||
</entry>
|
||||
ENTRY
|
||||
end
|
||||
|
||||
def header(person)
|
||||
"<header>#{plaintext_header}</header>"
|
||||
end
|
||||
|
||||
def plaintext_header
|
||||
header =<<HEADER
|
||||
<iv>#{iv}</iv>
|
||||
<aes_key>#{aes_key}</aes_key>
|
||||
<author>
|
||||
<name>#{@author.name}</name>
|
||||
<uri>acct:#{@author.diaspora_handle}</uri>
|
||||
</author>
|
||||
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
|
||||
|
|
@ -20,6 +20,24 @@ describe PublicsController do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#receive_public' do
|
||||
it 'succeeds' do
|
||||
post :receive_public, :xml => "<stuff/>"
|
||||
response.should be_success
|
||||
end
|
||||
|
||||
it 'returns a 422 if no xml is passed' do
|
||||
post :receive_public
|
||||
response.code.should == '422'
|
||||
end
|
||||
|
||||
it 'enqueues a ReceivePublicSalmon job' do
|
||||
xml = "stuff"
|
||||
Resque.should_receive(:enqueue).with(Job::ReceivePublicSalmon, xml)
|
||||
post :receive_public, :xml => xml
|
||||
end
|
||||
end
|
||||
|
||||
describe '#receive' do
|
||||
let(:xml) { "<walruses></walruses>" }
|
||||
|
||||
|
|
@ -39,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_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
|
||||
|
|
|
|||
|
|
@ -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_by_user_and_activity).and_return(salmon)
|
||||
salmon.should_receive(:xml_for).with(alice.person).and_return('what')
|
||||
@hydra.stub!(:queue)
|
||||
@hydra.stub!(:run)
|
||||
|
|
|
|||
93
spec/lib/postzord/receiver/local_post_batch_spec.rb
Normal file
93
spec/lib/postzord/receiver/local_post_batch_spec.rb
Normal file
|
|
@ -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
|
||||
111
spec/lib/postzord/receiver/public_spec.rb
Normal file
111
spec/lib/postzord/receiver/public_spec.rb
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# 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
|
||||
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
|
||||
|
||||
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)
|
||||
receiver.salmon.should_not be_nil
|
||||
end
|
||||
end
|
||||
|
||||
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 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 'enqueues a Job::ReceiveLocalBatch' do
|
||||
Resque.should_receive(:enqueue).with(Job::ReceiveLocalBatch, anything, anything)
|
||||
@receiver.perform!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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 '#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 '#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
|
||||
|
|
@ -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(: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::SalmonSlap.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)
|
||||
|
|
|
|||
57
spec/lib/salmon/encrypted_slap_spec.rb
Normal file
57
spec/lib/salmon/encrypted_slap_spec.rb
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# 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::EncryptedSlap do
|
||||
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 '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
|
||||
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 '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}
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
5
spec/lib/salmon/magic_sig_envelope_spec.rb
Normal file
5
spec/lib/salmon/magic_sig_envelope_spec.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Salmon::MagicSigEnvelope do
|
||||
|
||||
end
|
||||
63
spec/lib/salmon/slap_spec.rb
Normal file
63
spec/lib/salmon/slap_spec.rb
Normal file
|
|
@ -0,0 +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_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.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
|
||||
|
|
@ -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
|
||||
|
|
@ -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_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])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
29
spec/models/post_visibility_spec.rb
Normal file
29
spec/models/post_visibility_spec.rb
Normal file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue